﻿<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">
	<channel>
		<title>Cloud en français</title>
		<link>/</link>
		<description>Parlons du «cloud computing» en français</description>
		<copyright>Copyright © 2026</copyright>
		<pubDate>Tue, 31 Mar 2026 12:16:42 GMT</pubDate>
		<lastBuildDate>Tue, 31 Mar 2026 12:16:42 GMT</lastBuildDate>
		<item>
			<title>Ajouter Keycloak à une application .NET Aspire existante</title>
			<link>/posts/2026-03-31-keycloak-aspire.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/2026/03/keycloak-login.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2026-03-31-keycloak-aspire.html</guid>
			<pubDate>Tue, 31 Mar 2026 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;&lt;em&gt;À la fin de ce post, vous aurez un flux de connexion/déconnexion fonctionnel, appuyé par Keycloak, qui roule localement via Aspire et peut être déployé avec Docker Compose.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Si votre application Aspire n'a pas encore d'authentification, c'est la voie la plus rapide vers un vrai fournisseur d'identité. Ce tutoriel vous guide à travers l'intégration de Keycloak OIDC dans une application .NET Aspire + Blazor Server existante: de l'enregistrement dans AppHost jusqu'à l'interface de connexion/déconnexion, en s'appuyant sur le code de production de &lt;strong&gt;&lt;a href="https://github.com/fboucher/NoteBookmark"&gt;NoteBookmark&lt;/a&gt;&lt;/strong&gt;, un gestionnaire de signets open source bâti avec .NET Aspire et Blazor.&lt;/p&gt;
&lt;h2 id="etape-1-ajouter-aspire.hosting.keycloak-a-apphost"&gt;Étape 1: Ajouter Aspire.Hosting.Keycloak à AppHost&lt;/h2&gt;
&lt;p&gt;Aspire intègre nativement Keycloak via le paquet &lt;code&gt;Aspire.Hosting.Keycloak&lt;/code&gt;. Ajoutez-le à votre projet AppHost :&lt;/p&gt;
&lt;p&gt;Pour le projet AppHost&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;dotnet add package Aspire.Hosting.Keycloak
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensuite, lancez &lt;code&gt;dotnet restore&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="etape-2-enregistrer-keycloak-dans-apphost.cs"&gt;Étape 2: Enregistrer Keycloak dans AppHost.cs&lt;/h2&gt;
&lt;p&gt;Une fois le paquet installé, enregistrez Keycloak comme ressource dans votre AppHost. Aspire démarre un conteneur Keycloak, injecte ses détails de connexion dans les projets dépendants et s'assure que le démarrage se fait dans le bon ordre.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;
// ...

// Ajouter le serveur d'authentification Keycloak
var keycloak = builder.AddKeycloak("keycloak", port: 8080)
    .WithDataVolume(); // Persiste les données Keycloak entre les redémarrages

if (builder.Environment.IsDevelopment())
{
    // ...

    builder.AddProject&amp;lt;NoteBookmark_BlazorApp&amp;gt;("blazor-app")
        // ...
        .WithReference(keycloak)  // &amp;lt;-- référence Keycloak
        .WaitFor(keycloak)  // &amp;lt;-- attend que Keycloak soit prêt
        .WithExternalHttpEndpoints()
        .PublishAsDockerComposeService((resource, service) =&amp;gt;
        {
            service.ContainerName = "notebookmark-blazor";
        });
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="changements-cles"&gt;Changements clés :&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;AddKeycloak("keycloak", port: 8080)&lt;/code&gt;&lt;/strong&gt;: Enregistre une ressource Keycloak qui écoute sur le port 8080.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;WithDataVolume()&lt;/code&gt;&lt;/strong&gt;: Persiste la configuration de Keycloak et les données de realm entre les redémarrages du conteneur. Sans ça, vous perdriez votre configuration à chaque arrêt du conteneur.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.WithReference(keycloak)&lt;/code&gt;&lt;/strong&gt;: Injecte les paramètres de connexion Keycloak (URL de base, etc.) dans BlazorApp sous forme de variables d'environnement.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;.WaitFor(keycloak)&lt;/code&gt;&lt;/strong&gt;: Garantit que Keycloak est prêt avant le démarrage de Blazor. Si l'app démarre avant Keycloak, la découverte OIDC plante.&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note :&lt;/strong&gt; On ajoute la référence Keycloak seulement quand &lt;code&gt;IsDevelopment()&lt;/code&gt; est vrai. Ainsi, votre environnement de développement se met en place tout seul à chaque démarrage, avec un conteneur Keycloak créé automatiquement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="etape-3-configurer-keycloak-pour-les-deploiements-hors-aspire-prod"&gt;Étape 3: Configurer Keycloak pour les déploiements hors Aspire (prod)&lt;/h2&gt;
&lt;p&gt;Ce billet mise sur le développement avec Aspire, mais la production (Docker Compose, Kubernetes) exige un Keycloak autonome. Voici comment ça se présente et pourquoi.&lt;/p&gt;
&lt;p&gt;Aspire peut d'ailleurs vous aider à faire la transition. &lt;code&gt;AddDockerComposeEnvironment()&lt;/code&gt; dans AppHost génère un fichier Docker Compose préliminaire à partir de votre modèle Aspire, un excellent point de départ avant de personnaliser pour la production. Ça vaut le coup d'œil si vous voulez prendre de l'avance.&lt;/p&gt;
&lt;p&gt;Les fichiers Compose finaux pour Keycloak et l'application NoteBookmark sont disponibles dans le &lt;a href="https://github.com/fboucher/NoteBookmark/tree/main/docker-compose"&gt;dépôt NoteBookmark&lt;/a&gt; :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fboucher/NoteBookmark/blob/main/docker-compose/keycloak-compose.yaml"&gt;&lt;code&gt;keycloak-compose.yaml&lt;/code&gt;&lt;/a&gt;: Keycloak + Postgres, avec support de certificats TLS&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/fboucher/NoteBookmark/blob/main/docker-compose/note-compose.yaml"&gt;&lt;code&gt;note-compose.yaml&lt;/code&gt;&lt;/a&gt;: L'API NoteBookmark et l'application Blazor&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Quelques points à retenir sur cette configuration :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Postgres comme base de données&lt;/strong&gt;: Keycloak utilise une instance Postgres dédiée (pas la base de données de l'application) pour persister la configuration du realm, les utilisateurs et les sessions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Variables d'environnement via &lt;code&gt;.env&lt;/code&gt;&lt;/strong&gt;: Les identifiants (&lt;code&gt;POSTGRES_USER&lt;/code&gt;, &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;, &lt;code&gt;KEYCLOAK_USER&lt;/code&gt;, &lt;code&gt;KEYCLOAK_PASSWORD&lt;/code&gt;, &lt;code&gt;KEYCLOAK_URL&lt;/code&gt;) sont sortis du fichier Compose et chargés depuis un fichier &lt;code&gt;.env&lt;/code&gt; dans le même répertoire.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;KC_HTTP_ENABLED: "true"&lt;/code&gt;&lt;/strong&gt;: Autorise le trafic HTTP en interne. En production, Keycloak est derrière un reverse proxy (nginx, Traefik) qui gère la terminaison TLS: HTTPS à l'extérieur, HTTP à l'intérieur.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;KC_FEATURES: "token-exchange"&lt;/code&gt;&lt;/strong&gt;: Active la fonctionnalité d'échange de jetons, nécessaire si vous voulez des flux d'authentification service-à-service.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Réseau externe&lt;/strong&gt;: Les deux fichiers Compose partagent le même réseau Docker externe, permettant aux conteneurs de l'application d'atteindre Keycloak par son nom de conteneur.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="etape-4-configurer-le-realm-keycloak-et-le-client-oidc"&gt;Étape 4: Configurer le Realm Keycloak et le client OIDC&lt;/h2&gt;
&lt;p&gt;Cette étape s'applique autant en développement qu'en production, mais une seule fois par environnement. En développement, &lt;code&gt;.WithDataVolume()&lt;/code&gt; fait en sorte que la configuration Keycloak survit entre les sessions: vous configurez une fois, et c'est réglé.&lt;/p&gt;
&lt;p&gt;Une fois Keycloak en marche, configurez-le :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Naviguez vers &lt;code&gt;http://localhost:8080&lt;/code&gt; et connectez-vous avec vos identifiants d'administrateur.&lt;/li&gt;
&lt;li&gt;Créez un nouveau realm :
&lt;ul&gt;
&lt;li&gt;Cliquez sur &lt;strong&gt;Create Realm&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Nom: &lt;code&gt;notebookmark&lt;/code&gt; (doit correspondre au realm dans votre URL d'autorité ci-dessous)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Créez un client OIDC :
&lt;ul&gt;
&lt;li&gt;Clients → &lt;strong&gt;Create Client&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client ID&lt;/strong&gt;: &lt;code&gt;notebookmark&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Protocol&lt;/strong&gt;: openid-connect&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Access Type&lt;/strong&gt;: confidential (génère un secret client)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Valid Redirect URIs&lt;/strong&gt;: &lt;code&gt;http://localhost:5173/*&lt;/code&gt; (ajustez selon l'URL de votre application Blazor)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Web Origins&lt;/strong&gt;: &lt;code&gt;http://localhost:5173&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Allez dans l'onglet &lt;strong&gt;Credentials&lt;/strong&gt; et copiez le &lt;strong&gt;Client Secret&lt;/strong&gt;: vous en aurez besoin dans la configuration de votre application.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src="/content/images/2026/03/keycloak_secret.png" alt="Écran de configuration du client Keycloak"&gt;&lt;/p&gt;
&lt;h2 id="etape-5-ajouter-openid-connect-a-lapplication-blazor"&gt;Étape 5: Ajouter OpenID Connect à l'application Blazor&lt;/h2&gt;
&lt;p&gt;Place au pipeline d'authentification dans votre application Blazor Server.&lt;/p&gt;
&lt;h3 id="ajouter-le-paquet-nuget"&gt;Ajouter le paquet NuGet&lt;/h3&gt;
&lt;p&gt;Pour le projet BlazorApp&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="mettre-a-jour-program.cs"&gt;Mettre à jour Program.cs&lt;/h3&gt;
&lt;p&gt;BlazorApp/Program.cs :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;

//...

// Ajouter l'authentification
builder.Services.AddAuthentication(options =&amp;gt;
{
    options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =&amp;gt;
{
    var authority = builder.Configuration["Keycloak:Authority"];
    options.Authority = authority;
    options.ClientId = builder.Configuration["Keycloak:ClientId"];
    options.ClientSecret = builder.Configuration["Keycloak:ClientSecret"];
    options.ResponseType = "code";
    options.SaveTokens = true;
    options.GetClaimsFromUserInfoEndpoint = true;

    // Permet de remplacer RequireHttpsMetadata via la configuration.
    // Assouplit l'exigence en conteneur contre un Keycloak en HTTP.
    var requireHttpsConfigured = builder.Configuration.GetValue&amp;lt;bool?&amp;gt;("Keycloak:RequireHttpsMetadata");
    var isRunningInContainer = string.Equals(
        System.Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"),
        "true",
        StringComparison.OrdinalIgnoreCase);

    if (requireHttpsConfigured.HasValue)
    {
        options.RequireHttpsMetadata = requireHttpsConfigured.Value;
    }
    else
    {
        var defaultRequireHttps = !builder.Environment.IsDevelopment();
        if (isRunningInContainer &amp;amp;&amp;amp;
            !string.IsNullOrEmpty(authority) &amp;amp;&amp;amp;
            authority.StartsWith("http://", StringComparison.OrdinalIgnoreCase))
        {
            defaultRequireHttps = false;
        }
        options.RequireHttpsMetadata = defaultRequireHttps;
    }

    options.Scope.Clear();
    options.Scope.Add("openid");
    options.Scope.Add("profile");
    options.Scope.Add("email");

    options.TokenValidationParameters = new()
    {
        NameClaimType = "preferred_username",
        RoleClaimType = "roles"
    };

    // Configure la déconnexion pour passer id_token_hint à Keycloak
    options.Events = new OpenIdConnectEvents
    {
        OnRedirectToIdentityProviderForSignOut = async context =&amp;gt;
        {
            var idToken = await context.HttpContext.GetTokenAsync("id_token");
            if (!string.IsNullOrEmpty(idToken))
            {
                context.ProtocolMessage.IdTokenHint = idToken;
            }
        }
    };
});

builder.Services.AddAuthorization();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddHttpContextAccessor();

// ... Razor Components, FluentUI, etc. existants ...

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

// ... middleware existant ...

// CRITIQUE: UseAuthentication AVANT UseAuthorization
app.UseAuthentication();
app.UseAuthorization();

app.MapRazorComponents&amp;lt;App&amp;gt;()
    .AddInteractiveServerRenderMode();

// Points de terminaison d'authentification
app.MapGet("/authentication/login", async (HttpContext context, string? returnUrl) =&amp;gt;
{
    var authProperties = new AuthenticationProperties { RedirectUri = returnUrl ?? "/" };
    await context.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
});

app.MapGet("/authentication/logout", async (HttpContext context) =&amp;gt;
{
    var authProperties = new AuthenticationProperties { RedirectUri = "/" };
    await context.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await context.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
});

app.Run();
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="configuration"&gt;Configuration&lt;/h3&gt;
&lt;p&gt;Créez ou mettez à jour appsettings.json dans le projet BlazorApp :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  "Keycloak": {
    "Authority": "http://localhost:8080/realms/notebookmark",
    "ClientId": "notebookmark",
    "ClientSecret": "votre-secret-client-depuis-keycloak",
    "RequireHttpsMetadata": false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pour vos déploiements Docker Compose en production, utilisez des variables d'environnement dans votre &lt;code&gt;docker-compose.yaml&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;environment:
  Keycloak__Authority: ${KEYCLOAK_AUTHORITY}
  Keycloak__ClientId: ${KEYCLOAK_CLIENT_ID}
  Keycloak__ClientSecret: ${KEYCLOAK_CLIENT_SECRET}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;En environnement de développement, &lt;code&gt;.WithReference(keycloak)&lt;/code&gt; d'Aspire injecte automatiquement des variables d'environnement comme &lt;code&gt;services__keycloak__http__0&lt;/code&gt; pour l'URL de base de Keycloak. Vous pouvez les lire dans votre configuration ou définir manuellement l'URL d'autorité comme indiqué ci-dessus.&lt;/p&gt;
&lt;h3 id="le-piege-de-requirehttpsmetadata-http-vs-https"&gt;Le piège de RequireHttpsMetadata: HTTP vs HTTPS&lt;/h3&gt;
&lt;p&gt;Par défaut, le middleware OpenID Connect exige HTTPS pour la découverte des métadonnées (&lt;code&gt;RequireHttpsMetadata = true&lt;/code&gt;). Parfait en production, mais ça coince dans les environnements de développement local ou en conteneur où Keycloak tourne en HTTP.&lt;/p&gt;
&lt;p&gt;Le code ci-dessus met en place un repli intelligent :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;La config explicite a priorité&lt;/strong&gt;: Si &lt;code&gt;Keycloak:RequireHttpsMetadata&lt;/code&gt; est défini dans la config, on utilise cette valeur.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Détection de l'environnement conteneur&lt;/strong&gt;: Si l'application tourne dans un conteneur (&lt;code&gt;DOTNET_RUNNING_IN_CONTAINER=true&lt;/code&gt;) et que l'URL d'autorité est en HTTP, l'exigence HTTPS est désactivée.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;HTTPS par défaut en production&lt;/strong&gt;: En dehors du mode Development, HTTPS est exigé par défaut.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Au final :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Le développement local fonctionne sans friction avec Keycloak en HTTP&lt;/li&gt;
&lt;li&gt;La communication conteneur-à-conteneur fonctionne (HTTP en interne)&lt;/li&gt;
&lt;li&gt;La production exige HTTPS (en supposant que vous l'avez bien configuré)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note: En production, faites tourner Keycloak derrière un reverse proxy (nginx, Traefik, etc.) qui gère la terminaison TLS. Votre application voit &lt;code&gt;https://votredomaine.com&lt;/code&gt;, Keycloak roule en HTTP en interne.&lt;/p&gt;
&lt;p&gt;La configuration côté serveur est bouclée. Place aux composants Blazor qui rendent tout ça visible pour l'utilisateur.&lt;/p&gt;
&lt;h2 id="etape-6-interface-blazor-connexion-deconnexion-et-protection-des-routes"&gt;Étape 6: Interface Blazor (connexion, déconnexion et protection des routes)&lt;/h2&gt;
&lt;p&gt;Le pipeline backend en place, on peut maintenant bâtir les composants UI pour la connexion, la déconnexion et l'accès au contenu protégé. Trois éléments clés: les pages de connexion/déconnexion, un composant d'affichage et la gestion du routage avec les autorisations.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2026/03/keycloak-login.png" alt="Écran de connexion Keycloak"&gt;&lt;/p&gt;
&lt;h3 id="les-pages-razor-de-connexion-et-deconnexion"&gt;Les pages Razor de connexion et déconnexion&lt;/h3&gt;
&lt;p&gt;D'abord, des pages pour déclencher les flux d'authentification. Pas de balisage ici: ce sont de simples déclencheurs de redirection qui cèdent le contrôle à Keycloak.&lt;/p&gt;
&lt;h3 id="login.razor"&gt;Login.razor&lt;/h3&gt;
&lt;p&gt;Créez &lt;code&gt;Components/Pages/Login.razor&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-razor"&gt;@page "/login"
@attribute [AllowAnonymous]
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
@inject NavigationManager Navigation
@inject IHttpContextAccessor HttpContextAccessor

@code {
    protected override async Task OnInitializedAsync()
    {
        var uri = new Uri(Navigation.Uri);
        var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
        var returnUrl = query["returnUrl"] ?? "/";

        var httpContext = HttpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            var authProperties = new AuthenticationProperties
            {
                RedirectUri = returnUrl
            };
            await httpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, authProperties);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ce qui se passe ici ?&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pas de markup&lt;/strong&gt;: Cette page ne rend rien. Elle déclenche le défi OIDC, ce qui redirige le navigateur vers Keycloak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;ChallengeAsync&lt;/code&gt;&lt;/strong&gt;: Déclenche le middleware OIDC pour rediriger l'utilisateur vers la page de connexion Keycloak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL de retour&lt;/strong&gt;: On capte le paramètre &lt;code&gt;returnUrl&lt;/code&gt; pour que les utilisateurs reviennent là où ils étaient après la connexion.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;[AllowAnonymous]&lt;/code&gt;&lt;/strong&gt;: Critique ! Sans ça, la page exigerait une authentification pour y accéder, créant une boucle de redirection infinie.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="logout.razor"&gt;Logout.razor&lt;/h3&gt;
&lt;p&gt;Créez &lt;code&gt;Components/Pages/Logout.razor&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-razor"&gt;@page "/logout"
@attribute [AllowAnonymous]
@using Microsoft.AspNetCore.Authorization
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using Microsoft.AspNetCore.Authentication.OpenIdConnect
@inject IHttpContextAccessor HttpContextAccessor

@code {
    protected override async Task OnInitializedAsync()
    {
        var httpContext = HttpContextAccessor.HttpContext;
        if (httpContext != null)
        {
            var properties = new AuthenticationProperties
            {
                RedirectUri = "/"
            };
            await httpContext.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme, properties);
            await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Pourquoi se déconnecter de DEUX schémas ?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;C'est souvent là que ça déraille. OpenID Connect utilise un &lt;strong&gt;schéma d'authentification double&lt;/strong&gt; :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Schéma OpenIdConnect&lt;/strong&gt;: Gère le protocole avec Keycloak (redirections, échange de jetons, déconnexion).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Schéma Cookie&lt;/strong&gt;: Gère la session locale dans votre application Blazor.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Pour la déconnexion, vous devez vous déconnecter des deux, &lt;strong&gt;dans cet ordre&lt;/strong&gt; :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;OIDC en premier&lt;/strong&gt;: Redirige vers le point de terminaison de déconnexion de Keycloak, mettant fin à la session SSO.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cookie en second&lt;/strong&gt;: Efface le cookie d'authentification local.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Effacer seulement le cookie laisse la session Keycloak active, l'utilisateur peut cliquer sur « Connexion » et revenir instantanément, sans ressaisir ses identifiants. Se déconnecter seulement d'OIDC laisse le cookie local intact, et l'app continue de croire que l'utilisateur est connecté.&lt;/p&gt;
&lt;h2 id="etape-7-le-composant-logindisplay"&gt;Étape 7: Le composant LoginDisplay&lt;/h2&gt;
&lt;p&gt;Il nous faut maintenant un élément d'interface pour afficher l'état de connexion et fournir les actions de connexion/déconnexion. Il se trouve généralement dans l'en-tête ou la barre de navigation de votre application.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note :&lt;/strong&gt; NoteBookmark utilise FluentUI Blazor (les composants &lt;code&gt;&amp;lt;Fluent...&amp;gt;&lt;/code&gt;), ce n'est pas une obligation, mais ça a vraiment de l'allure ;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Créez &lt;code&gt;Components/Layout/LoginDisplay.razor&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-razor"&gt;@rendermode InteractiveServer
@using Microsoft.AspNetCore.Components.Authorization
@inject NavigationManager Navigation

&amp;lt;AuthorizeView&amp;gt;
    &amp;lt;Authorized&amp;gt;
        &amp;lt;FluentStack Orientation="Orientation.Horizontal" HorizontalGap="8"
                     HorizontalAlignment="HorizontalAlignment.Right"
                     VerticalAlignment="VerticalAlignment.Center"&amp;gt;
            &amp;lt;span&amp;gt;Bonjour, @context.User.Identity?.Name&amp;lt;/span&amp;gt;
            &amp;lt;FluentButton Appearance="Appearance.Lightweight" OnClick="Logout"
                          IconStart="@(new Icons.Regular.Size16.ArrowExit())"&amp;gt;
                Déconnexion
            &amp;lt;/FluentButton&amp;gt;
        &amp;lt;/FluentStack&amp;gt;
    &amp;lt;/Authorized&amp;gt;
    &amp;lt;NotAuthorized&amp;gt;
        &amp;lt;FluentButton Appearance="Appearance.Accent" OnClick="Login"
                      IconStart="@(new Icons.Regular.Size16.Person())"&amp;gt;
            Connexion
        &amp;lt;/FluentButton&amp;gt;
    &amp;lt;/NotAuthorized&amp;gt;
&amp;lt;/AuthorizeView&amp;gt;

@code {
    private void Login()
    {
        var returnUrl = Navigation.ToBaseRelativePath(Navigation.Uri);
        if (string.IsNullOrEmpty(returnUrl)) returnUrl = "/";
        Navigation.NavigateTo($"/login?returnUrl={Uri.EscapeDataString(returnUrl)}", forceLoad: false);
    }

    private void Logout()
    {
        Navigation.NavigateTo("/logout", forceLoad: false);
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Détails d'implémentation clés :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;@rendermode InteractiveServer&lt;/code&gt;&lt;/strong&gt;: Indispensable. &lt;code&gt;&amp;lt;AuthorizeView&amp;gt;&lt;/code&gt; dépend de &lt;code&gt;AuthenticationStateProvider&lt;/code&gt;, qui nécessite un rendu interactif. Sans ça, le composant s'affiche en HTML statique et reste sourd aux changements d'état d'authentification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;AuthorizeView&amp;gt;&lt;/code&gt;&lt;/strong&gt;: Ce composant affiche/masque automatiquement le contenu selon l'état d'authentification. Le paramètre &lt;code&gt;context&lt;/code&gt; donne accès au principal des claims de l'utilisateur.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;URL de retour à la connexion&lt;/strong&gt;: On passe l'URL de la page courante pour que les utilisateurs reviennent là où ils étaient après l'authentification.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;forceLoad: false&lt;/code&gt;&lt;/strong&gt;: On utilise la navigation interne à l'application. Les pages &lt;code&gt;Login.razor&lt;/code&gt; et &lt;code&gt;Logout.razor&lt;/code&gt; gèrent les vraies redirections HTTP.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Ajoutez ce composant à votre &lt;code&gt;MainLayout.razor&lt;/code&gt; ou à votre composant d'en-tête: &lt;code&gt;&amp;lt;LoginDisplay /&amp;gt;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2026/03/hello-frank.png" alt="Aperçu du composant LoginDisplay"&gt;&lt;/p&gt;
&lt;h2 id="etape-8-proteger-les-routes-et-les-pages"&gt;Étape 8: Protéger les routes et les pages&lt;/h2&gt;
&lt;p&gt;Avec la connexion/déconnexion en marche, il faut appliquer les règles d'autorisation. Blazor met à disposition deux mécanismes: la protection au niveau de la page avec &lt;code&gt;[Authorize]&lt;/code&gt; et la protection de contenu inline avec &lt;code&gt;&amp;lt;AuthorizeView&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="mettre-a-jour-routes.razor"&gt;Mettre à jour Routes.razor&lt;/h3&gt;
&lt;p&gt;Modifiez d'abord &lt;code&gt;Components/Routes.razor&lt;/code&gt; pour gérer le routage avec les autorisations :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-razor"&gt;@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Authorization

&amp;lt;FluentDesignTheme StorageName="theme" @rendermode="@InteractiveServer" /&amp;gt;

&amp;lt;CascadingAuthenticationState&amp;gt;
    &amp;lt;Router AppAssembly="typeof(Program).Assembly"&amp;gt;
        &amp;lt;Found Context="routeData"&amp;gt;
            &amp;lt;AuthorizeRouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)"&amp;gt;
                &amp;lt;NotAuthorized&amp;gt;
                    @if (context.User.Identity?.IsAuthenticated != true)
                    {
                        &amp;lt;FluentStack Orientation="Orientation.Vertical" VerticalGap="20"
                                     HorizontalAlignment="HorizontalAlignment.Center"
                                     Style="margin-top: 100px;"&amp;gt;
                            &amp;lt;FluentIcon Value="@(new Icons.Regular.Size48.LockClosed())" Color="Color.Accent" /&amp;gt;
                            &amp;lt;h2&amp;gt;Authentification requise&amp;lt;/h2&amp;gt;
                            &amp;lt;p&amp;gt;Vous devez être connecté pour accéder à cette page.&amp;lt;/p&amp;gt;
                            &amp;lt;FluentButton Appearance="Appearance.Accent"
                                OnClick="@(() =&amp;gt; NavigationManager.NavigateTo(
                                    "/login?returnUrl=" + Uri.EscapeDataString(
                                        NavigationManager.ToBaseRelativePath(NavigationManager.Uri)),
                                    forceLoad: false))"&amp;gt;
                                Connexion
                            &amp;lt;/FluentButton&amp;gt;
                        &amp;lt;/FluentStack&amp;gt;
                    }
                    else
                    {
                        &amp;lt;FluentStack Orientation="Orientation.Vertical" VerticalGap="20"
                                     HorizontalAlignment="HorizontalAlignment.Center"
                                     Style="margin-top: 100px;"&amp;gt;
                            &amp;lt;FluentIcon Value="@(new Icons.Regular.Size48.ShieldError())" Color="Color.Error" /&amp;gt;
                            &amp;lt;h2&amp;gt;Accès refusé&amp;lt;/h2&amp;gt;
                            &amp;lt;p&amp;gt;Vous n'avez pas la permission d'accéder à cette page.&amp;lt;/p&amp;gt;
                            &amp;lt;FluentButton Appearance="Appearance.Accent"
                                OnClick="@(() =&amp;gt; NavigationManager.NavigateTo("/", forceLoad: false))"&amp;gt;
                                Retour à l'accueil
                            &amp;lt;/FluentButton&amp;gt;
                        &amp;lt;/FluentStack&amp;gt;
                    }
                &amp;lt;/NotAuthorized&amp;gt;
            &amp;lt;/AuthorizeRouteView&amp;gt;
            &amp;lt;FocusOnNavigate RouteData="routeData" Selector="h1" /&amp;gt;
        &amp;lt;/Found&amp;gt;
    &amp;lt;/Router&amp;gt;
&amp;lt;/CascadingAuthenticationState&amp;gt;

@code {
    [Inject] private NavigationManager NavigationManager { get; set; } = default!;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Ce qui a changé ?&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;CascadingAuthenticationState&amp;gt;&lt;/code&gt;&lt;/strong&gt;: Enveloppe tout le routeur et rend l'état d'authentification disponible à tous les composants enfants. Sans ça, &lt;code&gt;&amp;lt;AuthorizeView&amp;gt;&lt;/code&gt; et les attributs &lt;code&gt;[Authorize]&lt;/code&gt; ne fonctionneront pas.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;AuthorizeRouteView&amp;gt;&lt;/code&gt;&lt;/strong&gt;: Remplace le &lt;code&gt;RouteView&lt;/code&gt; standard. Ce composant vérifie l'attribut &lt;code&gt;[Authorize]&lt;/code&gt; sur les pages routées et applique les règles d'autorisation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;&amp;lt;NotAuthorized&amp;gt;&lt;/code&gt; avec deux états&lt;/strong&gt;: C'est subtil mais important. Le bloc &lt;code&gt;&amp;lt;NotAuthorized&amp;gt;&lt;/code&gt; s'affiche quand l'autorisation échoue, mais deux situations se présentent :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Non authentifié&lt;/strong&gt; (&lt;code&gt;context.User.Identity?.IsAuthenticated != true&lt;/code&gt;): L'utilisateur n'est pas connecté → bouton « Connexion ».&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Authentifié mais non autorisé&lt;/strong&gt; (sinon): L'utilisateur est connecté mais sans les permissions requises → message « Accès refusé ».&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="proteger-les-pages-avec-authorize"&gt;Protéger les pages avec [Authorize]&lt;/h3&gt;
&lt;p&gt;Pour exiger l'authentification sur une page entière, ajoutez l'attribut &lt;code&gt;[Authorize]&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-razor"&gt;@page "/posts"
@attribute [Authorize]
@using Microsoft.AspNetCore.Authorization

&amp;lt;PageTitle&amp;gt;Mes billets&amp;lt;/PageTitle&amp;gt;

&amp;lt;h1&amp;gt;Mes billets&amp;lt;/h1&amp;gt;

&amp;lt;!-- Votre contenu protégé ici --&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les utilisateurs non authentifiés qui naviguent vers &lt;code&gt;/posts&lt;/code&gt; verront le message « Authentification requise » de &lt;code&gt;Routes.razor&lt;/code&gt;, pas le contenu de la page.&lt;/p&gt;
&lt;p&gt;Note: &lt;code&gt;[Authorize]&lt;/code&gt; supporte aussi les rôles et les politiques (ex.: &lt;code&gt;[Authorize(Roles = "Admin")]&lt;/code&gt;) pour un contrôle plus fin (matière à un futur billet).&lt;/p&gt;
&lt;p&gt;Tester le tout :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Démarrez votre hôte Aspire: &lt;code&gt;dotnet run --project NoteBookmark.AppHost&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Naviguez vers votre application Blazor dans le navigateur.&lt;/li&gt;
&lt;li&gt;Cliquez sur « Connexion »: vous devriez être redirigé vers Keycloak, vous authentifier et revenir.&lt;/li&gt;
&lt;li&gt;Vous verrez « Bonjour, [votre nom] » dans l'en-tête.&lt;/li&gt;
&lt;li&gt;Naviguez vers une page marquée &lt;code&gt;[Authorize]&lt;/code&gt; sans être connecté: vous verrez le message d'authentification requise.&lt;/li&gt;
&lt;li&gt;Cliquez sur « Déconnexion »: vous serez déconnecté de l'application et de Keycloak.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;L'authentification OpenID Connect est maintenant complète dans votre application Blazor, avec une séparation nette entre la mécanique (pages Login/Logout), l'interface (LoginDisplay) et l'application des règles (Routes.razor + [Authorize]).&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;L'intégration de Keycloak dans votre application .NET Aspire est complète. Les éléments clés :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Orchestration Aspire&lt;/strong&gt;: &lt;code&gt;AddKeycloak()&lt;/code&gt;, &lt;code&gt;.WithReference()&lt;/code&gt; et &lt;code&gt;.WaitFor()&lt;/code&gt; gèrent le cycle de vie des conteneurs et l'injection de configuration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipeline OIDC&lt;/strong&gt;: Le middleware d'authentification ASP.NET Core standard, configuré pour les points de terminaison OIDC de Keycloak.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Flexibilité HTTP&lt;/strong&gt;: Logique pour gérer Keycloak en HTTP en développement tout en imposant HTTPS en production.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Données persistantes&lt;/strong&gt;: &lt;code&gt;WithDataVolume()&lt;/code&gt; s'assure que la configuration de votre realm Keycloak survit aux redémarrages.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Cette approche dépasse largement Keycloak. Le modèle de ressources d'Aspire s'applique de la même façon aux bases de données, aux files de messages et à bien d'autres services. Dès que vous maîtrisez &lt;code&gt;.WithReference()&lt;/code&gt; et &lt;code&gt;.WaitFor()&lt;/code&gt;, vous pouvez assembler des systèmes distribués complexes en toute confiance.&lt;/p&gt;
&lt;p&gt;L'implémentation complète et fonctionnelle est disponible dans le &lt;strong&gt;&lt;a href="https://github.com/fboucher/NoteBookmark"&gt;dépôt NoteBookmark&lt;/a&gt;&lt;/strong&gt;, incluant la configuration AppHost, les composants Blazor et les fichiers Docker Compose référencés tout au long de ce billet.&lt;/p&gt;
&lt;h4 id="liens-utiles"&gt;Liens utiles&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/fboucher/NoteBookmark"&gt;Dépôt NoteBookmark&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.keycloak.org/documentation"&gt;Documentation Keycloak&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://aspire.dev/integrations/security/keycloak/"&gt;Documentation Aspire: intégration Keycloak&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Reka Edge en local: analyser des images et vidéos sans quitter votre machine</title>
			<link>/posts/2026-03-19-reka-edge-local.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/2026/03/header_notext_crop.jpg" length="0" type="image" />
			<guid isPermaLink="false">/posts/2026-03-19-reka-edge-local.html</guid>
			<pubDate>Mon, 16 Mar 2026 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;On a &lt;a href="https://reka.ai/news/reka-edge-frontier-level-edge-intelligence-for-physical-ai"&gt;lancé Reka Edge&lt;/a&gt; la semaine dernière chez Reka, et j'avais envie de vous montrer comment le faire tourner. C'est un modèle de vision-langage, en gros, une IA à qui vous pouvez montrer une image ou une vidéo et poser des questions dessus. Ce qui le distingue : &lt;strong&gt;tout se passe sur votre machine&lt;/strong&gt;. Pas de compte, pas de clé API, pas de données qui s'en vont quelque part sur un serveur.&lt;/p&gt;
&lt;p&gt;J'ai eu beaucoup de plaisir à préparer ce guide. Ça devrait vous prendre une quinzaine de minutes, café compris.&lt;/p&gt;
&lt;h2 id="ce-dont-vous-avez-besoin"&gt;Ce dont vous avez besoin&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Une machine avec ~16 Go de RAM (assez pour un modèle 7b)&lt;/li&gt;
&lt;li&gt;Git&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.astral.sh/uv/"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt;, un gestionnaire de paquets Python vraiment rapide :
&lt;pre&gt;&lt;code class="language-bash"&gt;curl -LsSf https://astral.sh/uv/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;
Ça fonctionne sur macOS, Linux et Windows via WSL. Si vous n'avez pas WSL, il y a aussi un &lt;a href="https://docs.astral.sh/uv/getting-started/installation/"&gt;installateur Windows natif&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="etape-1-cloner-le-depot"&gt;Étape 1 : Cloner le dépôt&lt;/h2&gt;
&lt;p&gt;Le modèle et le code d'inférence sont hébergés ensemble sur &lt;a href="https://huggingface.co/"&gt;Hugging Face&lt;/a&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git clone https://huggingface.co/RekaAI/reka-edge-2603
cd reka-edge-2603
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="etape-2-telecharger-les-poids-du-modele"&gt;Étape 2 : Télécharger les poids du modèle&lt;/h2&gt;
&lt;p&gt;Hugging Face utilise Git LFS pour les gros fichiers. Après le clone, les poids du modèle ne sont pas encore là — seulement des fichiers pointeurs. Il faut les récupérer séparément.&lt;/p&gt;
&lt;p&gt;Installez Git LFS si ce n'est pas déjà fait :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;# macOS
brew install git-lfs

# Linux / WSL (Ubuntu/Debian)
sudo apt install git-lfs
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Puis initialisez et tirez les fichiers :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git lfs install
git lfs pull
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les poids font plusieurs gigaoctets, alors c'est le bon moment pour aller chercher ce café.&lt;/p&gt;
&lt;h2 id="etape-3-poser-une-question-au-modele"&gt;Étape 3 : Poser une question au modèle&lt;/h2&gt;
&lt;p&gt;Le dépôt vient avec des exemples médias dans &lt;code&gt;media/&lt;/code&gt;. Pour analyser une image :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;uv run example.py \
  --image ./media/hamburger.jpg \
  --prompt "What is in this image?"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pour une vidéo, c'est le même principe avec &lt;code&gt;--video&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;uv run example.py \
  --video ./media/many_penguins.mp4 \
  --prompt "What is in this?"
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le modèle charge, traite votre fichier, et vous sort une description dans le terminal. Rien d'autre ne se passe — pas de requête réseau, pas de télémétrie.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2026/03/prompt_and_burger_800.png" alt="The prompt and the hamburger image"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quelques prompts intéressants à tester :&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"Describe this scene in detail."&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"What text is visible in this image?"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"Is there anything unusual or unexpected here?"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="ce-qui-se-passe-sous-le-capot"&gt;Ce qui se passe sous le capot&lt;/h2&gt;
&lt;p&gt;Vous n'avez pas besoin de lire ça pour utiliser le modèle, mais si vous êtes curieux comme moi, voici ce que &lt;code&gt;example.py&lt;/code&gt; fait vraiment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1. Il détecte le meilleur accélérateur disponible.&lt;/strong&gt;
GPU Nvidia (CUDA), Apple Silicon (Metal), ou CPU en dernier recours. La qualité des réponses ne change pas, juste la vitesse.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;if torch.cuda.is_available():
    device = torch.device("cuda")
elif mps_ok:
    device = torch.device("mps")
else:
    device = torch.device("cpu")
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;2. Il charge les poids en mémoire.&lt;/strong&gt;
Les 7 milliards de paramètres sont lus depuis le dossier cloné — ce sont des milliards de nombres qui encodent ce que le modèle a appris. Comptez environ 30 secondes selon votre matériel.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;processor = AutoProcessor.from_pretrained(args.model, trust_remote_code=True)
model = AutoModelForImageTextToText.from_pretrained(args.model, ...).eval()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3. Il formate l'entrée comme une conversation.&lt;/strong&gt;
Votre image et votre texte sont combinés dans un message structuré, un peu comme un chat — sauf qu'un des éléments est visuel.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;messages = [{
    "role": "user",
    "content": [
        {"type": "image", "image": args.image},
        {"type": "text", "text": args.prompt},
    ],
}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;4. Tout devient des chiffres.&lt;/strong&gt;
L'image est découpée en une grille de patches numériques, le texte est tokenisé. Le modèle ne travaille qu'avec des nombres — cette étape fait la conversion.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;inputs = processor.apply_chat_template(
    messages, tokenize=True, return_tensors="pt", return_dict=True
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;5. La réponse se génère token par token.&lt;/strong&gt;
Le modèle prédit le prochain mot le plus probable, encore et encore, jusqu'à 256 tokens ou jusqu'à ce qu'il juge la réponse complète.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;output_ids = model.generate(**inputs, max_new_tokens=256, do_sample=False)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;6. Les tokens redeviennent du texte.&lt;/strong&gt;
Les identifiants numériques sont décodés et affichés dans votre terminal. Aucune connexion réseau à aucune étape.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-python"&gt;output_text = processor.tokenizer.decode(new_tokens, skip_special_tokens=True)
print(output_text)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="la-suite"&gt;La suite&lt;/h2&gt;
&lt;p&gt;Un script, zéro dépendance cloud, et un modèle de vision de 7 milliards de paramètres qui tourne chez vous. Sur Mac, Linux, ou Windows avec WSL — c'est ce que j'utilisais quand j'ai écrit ce guide.&lt;/p&gt;
&lt;p&gt;Utilisé comme ça, c'est déjà pratique pour des questions ponctuelles. Mais si vous voulez brancher ça dans une vraie application — une app web, un outil qui surveille un dossier, quelque chose qui appelle le modèle en boucle — un script de ligne de commande commence à montrer ses limites.&lt;/p&gt;
&lt;p&gt;Dans le prochain article, je vais vous montrer comment exposer Edge comme une API locale. Même modèle, même confidentialité, mais accessible depuis n'importe quelle app sur votre machine.&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Créez automatiquement des clips IA avec ce template n8n</title>
			<link>/posts/2026-01-27-n8n-ai-creez-clip-auto.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/Cloudenfrancais-logo3.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2026-01-27-n8n-ai-creez-clip-auto.html</guid>
			<pubDate>Thu, 29 Jan 2026 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="creez-automatiquement-des-clips-ia-avec-ce-template-n8n"&gt;Créez automatiquement des clips IA avec ce template n8n&lt;/h1&gt;
&lt;p&gt;&lt;img src="/content/images/2026/01/cover_n8n_api_template_800.png" alt="Nouveau template n8n de découpage Reka"&gt;&lt;/p&gt;
&lt;p&gt;Je suis ravi de partager que mon nouveau template n8n a été approuvé et est maintenant disponible pour tout le monde! Ce template automatise le processus de création de clips vidéo générés par AI à partir de vidéos YouTube et envoie des notifications directement dans votre boîte courriel.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Essayez le template ici:&lt;/strong&gt; &lt;a href="https://link.reka.ai/n8n-template-api"&gt;https://link.reka.ai/n8n-template-api&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="quest-ce-que-ce-template-fait"&gt;Qu'est-ce que ce template fait?&lt;/h2&gt;
&lt;p&gt;Si vous avez toujours voulu créer automatiquement des courts clips à partir de longues vidéos YouTube, ce template est pour vous. Il surveille une chaîne YouTube de votre choix, et chaque fois qu'une nouvelle vidéo est publiée, il utilise l'AI pour générer des courts clips engageants parfaits pour les médias sociaux. Vous recevez une notification par courriel lorsque votre clip est prêt à télécharger.&lt;/p&gt;
&lt;h2 id="comment-ca-fonctionne"&gt;Comment ça fonctionne&lt;/h2&gt;
&lt;p&gt;Le flux de travail est simple et s'exécute complètement en pilote automatique:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Surveiller les chaînes YouTube&lt;/strong&gt; - Le template surveille le flux RSS de n'importe quelle chaîne YouTube que vous spécifiez. Lorsqu'une nouvelle vidéo apparaît, l'automation se déclenche.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Demander la génération de clip AI&lt;/strong&gt; - En utilisant l'API Vision de Reka, le flux de travail envoie la vidéo pour traitement AI. Vous avez le contrôle complet sur la sortie:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rédigez une instruction personnalisée pour guider l'AI sur le type de clip à créer&lt;/li&gt;
&lt;li&gt;Choisissez d'inclure ou non des sous-titres&lt;/li&gt;
&lt;li&gt;Définissez la durée minimale et maximale du clip&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Vérification intelligente du statut&lt;/strong&gt; - Lorsque les clips sont prêts, vous recevez un courriel de succès avec votre lien de téléchargement. Comme mesure de sécurité, si la tâche prend trop de temps, vous recevrez plutôt une notification d'erreur.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="demarrer-est-facile"&gt;Démarrer est facile&lt;/h2&gt;
&lt;p&gt;Le meilleur? Vous pouvez installer ce template en un seul clic depuis la &lt;a href="https://link.reka.ai/n8n-template-api"&gt;page des templates n8n&lt;/a&gt;. Aucune configuration complexe requise!&lt;/p&gt;
&lt;p&gt;Après l'installation, vous n'aurez besoin que de deux choses rapides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Une clé API Reka AI gratuite (&lt;a href="https://link.reka.ai/free"&gt;obtenez la vôtre chez Reka&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Un compte Gmail (ou utilisez n'importe quel fournisseur de courriel que vous aimez)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C'est tout! Le template est prêt à utiliser. Ajoutez simplement le flux RSS de votre chaîne YouTube, connectez votre clé API, et vous êtes prêt à commencer à générer des clips automatiquement. La configuration complète ne prend que quelques minutes.&lt;/p&gt;
&lt;p&gt;Si vous avez des questions ou voulez partager ce que vous avez créé, rejoignez la &lt;a href="https://link.reka.ai/discord"&gt;communauté Discord de Reka&lt;/a&gt;. J'aimerais beaucoup savoir comment vous utilisez ce template!&lt;/p&gt;
&lt;h2 id="regardez-le-en-action"&gt;Regardez-le en action&lt;/h2&gt;
&lt;p&gt;Dans cette vidéo rapide, je vous montre comment installer et configurer le template n8n pour créer automatiquement le template n8n.&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/SGAltjnBaF4?si=42-EpCEnra6Xwk8r" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;p&gt;Bon clipping!&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>De zéro à n8n: créer son premier node personnalisé</title>
			<link>/posts/2026-01-20-de-zero-a-n8n-noeud-personnalise.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/Cloudenfrancais-logo3.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2026-01-20-de-zero-a-n8n-noeud-personnalise.html</guid>
			<pubDate>Tue, 20 Jan 2026 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="de-zero-a-n8n-creer-son-premier-nud-personnalise"&gt;De zéro à n8n : créer son premier nœud personnalisé&lt;/h1&gt;
&lt;p&gt;Récemment, j'ai décidé de créer un cutom node pour &lt;a href="https://n8n.io/"&gt;n8n&lt;/a&gt;, l'outil d'automatisation de flux de travail que j'utilise. Je ne suis pas un expert en développement Node.js, mais je voulais comprendre comment les node n8n fonctionnent sous le capot. Cet article partage mon parcours et les étapes qui ont réellement fonctionné pour moi.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2026/01/n8n-node-build-successful_600.png" alt="n8n node build successful"&gt;&lt;/p&gt;
&lt;h2 id="pourquoi-jai-fait-ca"&gt;Pourquoi j'ai fait ça&lt;/h2&gt;
&lt;p&gt;Avant de commencer ce projet, j'étais curieux de savoir comment les node n8n sont construits. La meilleure façon d'apprendre quelque chose est de le faire, alors j'ai décidé de créer un cutom node simple en suivant le tutoriel officiel de n8n. Maintenant que je comprends les bases, je prévois construire un node plus complexe avec des capacités d'IA Vision, mais c'est pour un autre billet sur ce blogue!&lt;/p&gt;
&lt;h2 id="le-defi"&gt;Le défi&lt;/h2&gt;
&lt;p&gt;J'ai commencé avec le tutoriel officiel de n8n: &lt;a href="https://docs.n8n.io/integrations/creating-nodes/build/declarative-style-node/"&gt;Build a declarative-style node&lt;/a&gt;. Bien que le tutoriel soit bien écrit, j'ai rencontré quelques problèmes en cours de route. Les étapes ne fonctionnaient pas exactement comme décrit, alors j'ai dû comprendre ce qui manquait. Cet article documente ce qui a réellement fonctionné pour moi, au cas où vous feriez face à des défis similaires. J'ai déjà une instance de n8n qui roule dans un conteneur. À l'&lt;strong&gt;étape 8&lt;/strong&gt;, j'expliquerai comment j'exécute une deuxième instance pour des fins de développement.&lt;/p&gt;
&lt;h2 id="prerequis"&gt;Prérequis&lt;/h2&gt;
&lt;p&gt;Avant de commencer, vous aurez besoin de :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Node.js et npm&lt;/strong&gt; - J'ai utilisé Node.js version 24.12.0&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compréhension de base de JavaScript/TypeScript&lt;/strong&gt; - vous n'avez pas besoin d'être un expert&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="etape-1-corriger-les-prerequis-manquants"&gt;Étape 1 : Corriger les prérequis manquants&lt;/h2&gt;
&lt;p&gt;Je n'avais pas Node.js installé sur ma machine, donc ma première étape a été de régler ça. Au lieu d'installer Node.js directement, j'ai utilisé &lt;strong&gt;nvm&lt;/strong&gt; (Node Version Manager), qui facilite la gestion de différentes versions de Node.js. Les détails d'installation sont disponibles sur le &lt;a href="https://github.com/nvm-sh/nvm"&gt;dépôt GitHub de nvm&lt;/a&gt;. Une fois nvm configuré, j'ai installé Node.js version 24.12.0.&lt;/p&gt;
&lt;p&gt;La plupart du temps, j'utilise VS Code comme éditeur de code. J'ai créé un nouveau profil et utilisé le modèle pour le développement Node.js afin d'obtenir les bonnes extensions et paramètres.&lt;/p&gt;
&lt;h2 id="etape-2-cloner-le-depot-de-demarrage"&gt;Étape 2 : Cloner le dépôt de démarrage&lt;/h2&gt;
&lt;p&gt;n8n fournit un &lt;a href="https://github.com/n8n-io/n8n-nodes-starter"&gt;n8n-nodes-starter sur GitHub&lt;/a&gt; qui inclut tous les fichiers de base et les dépendances dont vous avez besoin. Vous pouvez le cloner ou l'utiliser comme modèle pour votre propre projet. Puisque c'était seleument un « exercice d'apprentissage » pour moi, j'ai cloné le dépôt directement :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;git clone https://github.com/n8n-io/n8n-nodes-starter
cd n8n-nodes-starter
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="etape-3-commencer-avec-le-tutoriel"&gt;Étape 3 : Commencer avec le tutoriel&lt;/h2&gt;
&lt;p&gt;Je ne répéterai pas le tutoriel complet ici; il est assez clair, mais je soulignerai quelques détails en cours de route que j'ai trouvés utiles.&lt;/p&gt;
&lt;p&gt;Le tutoriel vous fait créer un node « NasaPics » et fournit un logo pour celui-ci. C'est super, mais je suggère d'utiliser vos propres images de logo et d'avoir des versions claires et sombres. Ajoutez les deux images dans un nouveau dossier &lt;code&gt;icons&lt;/code&gt; (au même niveau que les dossiers &lt;code&gt;nodes&lt;/code&gt; et &lt;code&gt;credentials&lt;/code&gt;). Avoir deux versions du logo rendra votre node plus beau, peu importe le thème que l'utilisateur utilise dans n8n (clair ou sombre). Le tutoriel ajoute seulement le logo dans &lt;code&gt;NasaPics.node.ts&lt;/code&gt;, mais j'ai trouvé que l'ajouter aussi dans le fichier de credentials &lt;code&gt;NasaPicsApi.credentials.ts&lt;/code&gt; rend le node plus cohérent.&lt;/p&gt;
&lt;p&gt;Remplacez ou ajoutez la ligne du logo avec ceci, et ajoutez &lt;code&gt;Icon&lt;/code&gt; à la déclaration d'importation en haut du fichier :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-typescript"&gt;icon: Icon = { light: 'file:MyLogo-dark.svg', dark: 'file:MyLogo-light.svg' };
&lt;/code&gt;&lt;/pre&gt;
&lt;blockquote&gt;
&lt;p&gt;Note : le logo plus foncé devrait être utilisé en mode clair, et vice versa.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="etape-4-suivre-le-tutoriel-avec-ajustements"&gt;Étape 4 : Suivre le tutoriel (avec ajustements)&lt;/h2&gt;
&lt;p&gt;C'est là que les choses sont devenues intéressantes. J'ai suivi le tutoriel officiel pour créer les fichiers de node, mais j'ai dû faire quelques ajustements qui n'étaient pas mentionnés dans la documentation.&lt;/p&gt;
&lt;h3 id="ajustement-1-rendre-le-node-utilisable-comme-outil"&gt;Ajustement 1 : Rendre le node utilisable comme outil&lt;/h3&gt;
&lt;p&gt;Dans le fichier &lt;code&gt;NasaPics.node.ts&lt;/code&gt;, j'ai ajouté cette ligne juste avant le tableau &lt;code&gt;properties&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-typescript"&gt;requestDefaults: {
      baseURL: 'https://api.nasa.gov',
      headers: {
         Accept: 'application/json',
         'Content-Type': 'application/json',
      },
   },
   usableAsTool: true, // &amp;lt;-- Ajouté cette ligne
   properties: [
      // Resources and operations will go here
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ce paramètre permet au node d'être utilisé comme outil dans les flux de travail n8n et corrige aussi les avertissements de l'outil de lint.&lt;/p&gt;
&lt;h3 id="ajustement-2-securiser-le-champ-de-cle-api"&gt;Ajustement 2 : Sécuriser le champ de clé API&lt;/h3&gt;
&lt;p&gt;Dans le fichier &lt;code&gt;NasaPicsApi.credentials.ts&lt;/code&gt;, j'ai ajouté un &lt;code&gt;typeOptions&lt;/code&gt; pour faire du champ de clé API un champ de mot de passe. Cela garantit que la clé API est cachée lorsque les utilisateurs la saisissent, ce qui est une bonne pratique de sécurité.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-typescript"&gt;properties: INodeProperties[] = [
   {
      displayName: 'API Key',
      name: 'apiKey',
      type: 'string',
      typeOptions: { password: true }, // &amp;lt;-- Ajouté cette ligne
      default: '',
   },
];
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="une-note-sur-les-erreurs"&gt;Une note sur les erreurs&lt;/h3&gt;
&lt;p&gt;J'ai remarqué qu'il y avait quelques autres erreurs qui apparaissaient dans le fichier de credentials. Si vous lisez le message d'erreur, vous verrez qu'il se plaint de propriétés &lt;code&gt;test&lt;/code&gt; manquantes. Pour corriger cela, j'ai ajouté une propriété &lt;code&gt;test&lt;/code&gt; à la fin de la classe qui implémente &lt;code&gt;ICredentialTestRequest&lt;/code&gt;. J'ai aussi ajouté l'importation de l'interface en haut du fichier.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-typescript"&gt;authenticate: IAuthenticateGeneric = {
   type: 'generic',
   properties: {
      qs: {
         api_key: '={{$credentials.apiKey}}',
      },
   },
};

// Ajoutez ceci à la fin de la classe
test: ICredentialTestRequest = {
   request: {
      baseURL: 'https://api.nasa.gov/',
      url: '/user',
      method: 'GET',
   },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="etape-5-construire-et-lier-le-paquet"&gt;Étape 5 : Construire et lier le paquet&lt;/h2&gt;
&lt;p&gt;Une fois que j'avais tous mes fichiers prêts, il était temps de construire le node. Depuis la racine du dossier de mon projet de node, j'ai exécuté :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;npm i
npm run build
npm link
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Pendant le processus de construction, faites attention au nom du paquet qui est généré. Dans mon cas, c'était &lt;code&gt;n8n-nodes-nasapics&lt;/code&gt;. Vous aurez besoin de ce nom dans les prochaines étapes.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;&amp;gt; n8n-nodes-nasapics@0.1.0 build
&amp;gt; n8n-node build

┌   n8n-node build
│
◓  Building TypeScript files│
◇  TypeScript build successful
│
◇  Copied static files
│
└  ✓ Build successful
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="etape-6-configurer-le-dossier-personnalise-de-n8n"&gt;Étape 6 : Configurer le dossier personnalisé de n8n&lt;/h2&gt;
&lt;p&gt;n8n cherche les node personnalisés dans un emplacement spécifique : &lt;code&gt;~/.n8n/custom/&lt;/code&gt;. Si ce dossier n'existe pas, vous devez le créer :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;mkdir -p ~/.n8n/custom
cd ~/.n8n/custom
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensuite, initialisez un nouveau paquet npm dans ce dossier : exécutez &lt;code&gt;npm init&lt;/code&gt; et appuyez sur Entrée pour accepter tous les défauts.&lt;/p&gt;
&lt;h2 id="etape-7-lier-votre-node-a-n8n"&gt;Étape 7 : Lier votre node à n8n&lt;/h2&gt;
&lt;p&gt;Maintenant vient la partie magique - lier votre cutom node pour que n8n puisse le trouver. Remplacez &lt;code&gt;n8n-nodes-nasapics&lt;/code&gt; par le nom de votre paquet. Depuis le dossier &lt;code&gt;~/.n8n/custom&lt;/code&gt;, exécutez :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;npm link n8n-nodes-nasapics
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="etape-8-executer-n8n"&gt;Étape 8 : Exécuter n8n&lt;/h2&gt;
&lt;p&gt;C'est ici que ma configuration diffère du tutoriel standard. Comme mentionné au début, j'ai déjà une instance de n8n qui roule dans un conteneur et je ne voulais pas l'installer. Alors j'ai décidé d'exécuter un deuxième conteneur en utilisant un port différent. Voici la commande que j'ai utilisée :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-bash"&gt;docker run -d --name n8n-DEV -p 5680:5678 \
  -e N8N_COMMUNITY_PACKAGES_ENABLED=true \
  -v ~/.n8n/custom/node_modules/n8n-nodes-nasapics:/home/node/.n8n/custom/node_modules/n8n-nodes-nasapics \
  n8nio/n8n
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Laissez-moi expliquer ce que cette commande fait :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-d&lt;/code&gt; : Exécute le conteneur en mode détaché (en arrière-plan)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--name n8n-DEV&lt;/code&gt; : Nomme le conteneur pour une référence facile&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-p 5680:5678&lt;/code&gt; : Mappe le port 5678 du conteneur au port 5680 sur ma machine pour qu'il n'entre pas en conflit avec mon instance existante de n8n&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-e N8N_COMMUNITY_PACKAGES_ENABLED=true&lt;/code&gt; : Active les paquets communautaires — vous en avez besoin pour utiliser des node personnalisés&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-v&lt;/code&gt; : Monte mon dossier de cutom node dans le conteneur, ce qui me permet d'essayer mon cutom node sans avoir à le publier.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n8nio/n8n&lt;/code&gt; : L'image de conteneur officielle de n8n&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Si vous exécutez n8n directement sur votre machine (pas dans un conteneur), vous pouvez simplement le démarrer.&lt;/p&gt;
&lt;h2 id="etape-9-tester-votre-node"&gt;Étape 9 : Tester votre node&lt;/h2&gt;
&lt;p&gt;Une fois que &lt;strong&gt;n8n-DEV&lt;/strong&gt; roule, ouvrez votre navigateur et naviguez vers celui-ci. Créez un nouveau flux de travail et recherchez votre node. Dans mon cas, j'ai cherché « NasaPics » et mon cutom node est apparu!&lt;/p&gt;
&lt;p&gt;Pour le tester :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Ajoutez votre node au flux de travail&lt;/li&gt;
&lt;li&gt;Configurez les credentials avec une clé API de la NASA (vous pouvez en obtenir une gratuitement sur &lt;a href="https://api.nasa.gov/"&gt;api.nasa.gov&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Exécutez le node&lt;/li&gt;
&lt;li&gt;Vérifiez si les données sont récupérées correctement&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="mettre-a-jour-votre-node"&gt;Mettre à jour votre node&lt;/h2&gt;
&lt;p&gt;Pendant le développement, vous devrez probablement apporter des modifications à votre code (aka node). Une fois terminé, vous devez reconstruire &lt;code&gt;npm run build&lt;/code&gt; et redémarrer le conteneur n8n &lt;code&gt;docker restart n8n-DEV&lt;/code&gt; pour voir les changements.&lt;/p&gt;
&lt;h2 id="quelle-est-la-suite"&gt;Quelle est la suite?&lt;/h2&gt;
&lt;p&gt;Maintenant que je comprends les bases de la construction de node personnalisés n8n, je suis prêt à m'attaquer à quelque chose de plus ambitieux. Mon prochain projet sera de créer un node qui utilise des capacités d'IA Vision. Alerte au spoiler : C'est fait et je partagerai les détails dans un prochain article de blogue!&lt;/p&gt;
&lt;p&gt;Si vous êtes intéressé à créer vos propres node personnalisés, je vous encourage à essayer. Commencez avec quelque chose de simple, comme je l'ai fait, et construisez à partir de là. N'ayez pas peur d'expérimenter et de faire des erreurs, c'est comme ça qu'on apprend!&lt;/p&gt;
&lt;h4 id="ressources"&gt;Ressources&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/"&gt;Documentation officielle de n8n&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.n8n.io/integrations/creating-nodes/build/declarative-style-node/"&gt;Tutoriel Build a declarative-style node&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/n8n-io/n8n-nodes-starter"&gt;Dépôt n8n-nodes-starter&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/nvm-sh/nvm"&gt;nvm (Node Version Manager)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://community.n8n.io/"&gt;Forum communautaire de n8n&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Exposer des conteneurs d'un homelab avec Traefik et Cloudflare Tunnel</title>
			<link>/posts/2026-01-07-treafik-cloudflare-setup-fr.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/Cloudenfrancais-logo3.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2026-01-07-treafik-cloudflare-setup-fr.html</guid>
			<pubDate>Wed, 07 Jan 2026 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;J'adore le cloud, en fait la plupart des gens me connaissent probablement grâce à mon contenu partagé à ce sujet. Mais parfois, nos applications n'ont pas besoin de mise à l'échelle ou de redondance. Parfois, on veut simplement les héberger quelque part.&lt;/p&gt;
&lt;p&gt;C'était les vacances, et pendant mon congé, j'ai travaillé sur quelques petits projets personnels. Je les ai empaquetés dans des conteneurs pour faciliter le déploiement n'importe où. Je les ai déployés sur un mini PC que j'ai à la maison et c'est génial... tant que je reste à la maison. Mais que faire si je veux y accéder depuis ailleurs (ex. : la maison de mes beaux parents) ?&lt;/p&gt;
&lt;p&gt;J'ai configuré un beau tunnel Cloudflare vers un conteneur Traefik qui achemine le trafic vers le bon conteneur selon le préfixe ou le domaine de second niveau. Donc &lt;code&gt;dev.c5m.ca&lt;/code&gt; va au conteneur X et &lt;code&gt;test.c5m.ca&lt;/code&gt; va au conteneur Y. Dans ce billet, je voulais partager comment je l'ai fait (et aussi l'avoir quelque part pour moi au cas où j'aurais besoin de le refaire 😉). C'est simple une fois qu'on sait comment toutes les pièces fonctionnent ensemble.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2026/01/brick-container-light_800.jpeg" alt="exposer un conteneur"&gt;&lt;/p&gt;
&lt;div style="text-align: center;"&gt;&lt;i&gt;&lt;span style="color: #999999;"&gt;generated by Microsoft designer&lt;/span&gt;&lt;/i&gt;&lt;/div&gt;
&lt;h2 id="la-configuration"&gt;La configuration&lt;/h2&gt;
&lt;p&gt;L'architecture est simple : Cloudflare Tunnel crée une connexion sécurisée de mon réseau domestique vers le réseau périphérique de Cloudflare, et Traefik agit comme un proxy inverse qui route dynamiquement les requêtes entrantes vers le conteneur approprié selon le sous-domaine. De cette façon, je peux accéder à plusieurs services via différents sous-domaines sans exposer mon réseau domestique directement à Internet.&lt;/p&gt;
&lt;h2 id="etape-1-cloudflare-tunnel"&gt;Étape 1 : Cloudflare Tunnel&lt;/h2&gt;
&lt;p&gt;D'abord, en supposant que vous possédez déjà un nom de domaine, vous devrez créer un tunnel Cloudflare. Vous pouvez le faire via le tableau de bord Cloudflare sous Zero Trust → Networks → Tunnels. Une fois créé, vous obtiendrez un jeton de tunnel que vous utiliserez dans la configuration.&lt;/p&gt;
&lt;p&gt;Voici mon &lt;code&gt;cloudflare-docker-compose.yaml&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: cloudflare-tunnel

services:
  cloudflared:
    image: cloudflare/cloudflared:latest
    container_name: cloudflared
    restart: unless-stopped
    env_file:
      - .env
    environment:
      - TUNNEL_TOKEN=${TUNNEL_TOKEN}
    command: ["tunnel", "--no-autoupdate", "run", "--token", "${TUNNEL_TOKEN}"]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Le jeton de tunnel est stocké dans un fichier &lt;code&gt;.env&lt;/code&gt; pour la sécurité. Le drapeau &lt;code&gt;--no-autoupdate&lt;/code&gt; empêche le conteneur d'essayer de se mettre à jour automatiquement, ce qui est utile dans un environnement contrôlé.&lt;/p&gt;
&lt;h2 id="etape-2-configuration-dns"&gt;Étape 2 : Configuration DNS&lt;/h2&gt;
&lt;p&gt;Dans le tableau de bord Cloudflare, créez un enregistrement &lt;code&gt;CNAME&lt;/code&gt; avec un caractère générique &lt;code&gt;*.c5m.ca&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="etape-3-configuration-traefik"&gt;Étape 3 : Configuration Traefik&lt;/h2&gt;
&lt;p&gt;Traefik est le proxy inverse qui routera le trafic vers vos conteneurs. J'ai deux fichiers de configuration : un pour Traefik lui-même et un pour la configuration Docker Compose.&lt;/p&gt;
&lt;p&gt;Voici mon &lt;code&gt;traefik.yaml&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;global:
  checkNewVersion: false
  sendAnonymousUsage: false

api:
  dashboard: false #true
  insecure: true

entryPoints:
  web:
    address: :8082
  websecure:
    address: :8043

providers:
  docker:
    endpoint: "unix:///var/run/docker.sock"
    exposedByDefault: false 
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;J'ai configuré deux points d'entrée : &lt;code&gt;web&lt;/code&gt; sur le port 8082 (HTTP) et &lt;code&gt;websecure&lt;/code&gt; sur le port 8043 (HTTPS). Je l'ai fait ainsi parce que les ports par défaut 80 et 443 étaient déjà pris. Le fournisseur Docker surveille les conteneurs avec des étiquettes Traefik et configure automatiquement le routage. &lt;code&gt;exposedByDefault: false&lt;/code&gt; signifie que les conteneurs ne seront pas exposés à moins d'être explicitement activés avec des étiquettes. Vous n'aurez pas à modifier la configuration Traefik pour ajouter plus de conteneurs, tout est dynamique.&lt;/p&gt;
&lt;p&gt;Et voici le &lt;code&gt;traefik-docker-compose.yaml&lt;/code&gt; :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: traefik

services:
  traefik:
    image: "traefik:v3.4"
    container_name: "traefik-app"
    restart: unless-stopped
    networks:
      - proxy

    ports:
      - "8888:8080" # Dashboard port
      - "8082:8082"
      - "8043:8043" # remap 443
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      - "./config/traefik.yaml:/etc/traefik/traefik.yaml:ro"

networks:
  proxy:
    name: proxy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les points clés ici :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Traefik est connecté à un réseau Docker appelé &lt;code&gt;proxy&lt;/code&gt; qui sera partagé avec d'autres conteneurs. Vous pouvez le nommer comme vous voulez.&lt;/li&gt;
&lt;li&gt;Le port 8888 mappe au tableau de bord de Traefik (actuellement désactivé dans la config)&lt;/li&gt;
&lt;li&gt;Les ports 8082 et 8043 sont exposés pour le trafic HTTP et HTTPS&lt;/li&gt;
&lt;li&gt;La socket Docker est montée en lecture seule pour que Traefik puisse découvrir les conteneurs&lt;/li&gt;
&lt;li&gt;Le fichier de configuration est monté depuis &lt;code&gt;./config/traefik.yaml&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="etape-4-configuration-des-services"&gt;Étape 4 : Configuration des services&lt;/h2&gt;
&lt;p&gt;Maintenant, tout conteneur que vous voulez exposer via Traefik doit :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Être sur le même réseau &lt;code&gt;proxy&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Avoir des étiquettes Traefik configurées&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Voici un exemple simple avec un conteneur nginx (&lt;code&gt;nginx-docker-compose.yaml&lt;/code&gt;) :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: "test-tools"

services:
  nginx:
    image: "nginx:latest"
    container_name: "nginx-test"
    restart: unless-stopped
    networks:
      - proxy
    volumes:
      - "./html:/usr/share/nginx/html:ro"
      
    labels:
      - traefik.enable=true
      - traefik.http.routers.nginxtest.rule=Host(`test.c5m.ca`) 
      - traefik.http.routers.nginxtest.entrypoints=web

networks:
  proxy:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Les étiquettes indiquent à Traefik :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;traefik.enable=true&lt;/code&gt; : Ce conteneur devrait être exposé&lt;/li&gt;
&lt;li&gt;&lt;code&gt;nginxtest&lt;/code&gt; est le nom &lt;strong&gt;unique&lt;/strong&gt; pour router ce conteneur.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;traefik.http.routers.nginxtest.rule=Host(...)&lt;/code&gt; : Router les requêtes pour &lt;code&gt;test.c5m.ca&lt;/code&gt; vers ce conteneur&lt;/li&gt;
&lt;li&gt;&lt;code&gt;traefik.http.routers.nginxtest.entrypoints=web&lt;/code&gt; : Utiliser le point d'entrée &lt;code&gt;web&lt;/code&gt; (port 8082)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="bonus-un-exemple-plus-complexe"&gt;Bonus : Un exemple plus complexe&lt;/h2&gt;
&lt;p&gt;Pour un scénario plus réaliste, partageons comment je pourrais exposer &lt;a href="https://github.com/FBoucher/2d6-dungeon-app"&gt;2D6 Dungeon App&lt;/a&gt;. C'est un projet qui utilise Aspire, donc c'est facile à déployer comme on veut, où on veut.
Voici une version simplifiée de mon &lt;code&gt;2d6-docker-compose.yaml&lt;/code&gt; qui inclut une application multi-conteneurs :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;name: 2d6-dungeon

services:
  database:
    container_name: 2d6_db
    ports:
      - "${MYSQL_PORT:-3306}:3306"
    networks:
      - proxy
    ...

  dab:
    container_name: 2d6_dab
    ...
    depends_on:
      database:
        condition: service_healthy
    ports:
      - "${DAB_PORT:-5000}:5000"
    networks:
      - proxy

  webapp:
    container_name: 2d6_app
    depends_on:
      - dab
    environment:
      ConnectionStrings__dab: http://dab:5000
      services__dab__http__0: http://dab:5000

    labels:
      - traefik.enable=true
      - traefik.http.routers.twodsix.rule=Host(`2d6.c5m.ca`)
      - traefik.http.routers.twodsix.entrypoints=web,websecure
      - traefik.http.services.twodsix.loadbalancer.server.port=${WEBAPP_PORT:-8080}

    networks:
      - proxy

    ports:
      - "${WEBAPP_PORT:-8080}:${WEBAPP_PORT:-8080}"

networks:
  proxy:
    external: true
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Cet exemple montre :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Plusieurs services qui fonctionnent ensemble (base de données, API, application web)&lt;/li&gt;
&lt;li&gt;Seule l'application web est exposée via Traefik (la base de données et l'API sont internes)&lt;/li&gt;
&lt;li&gt;L'application web utilise les deux points d'entrée &lt;code&gt;web&lt;/code&gt; et &lt;code&gt;websecure&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Note importante ici : les conteneurs faisant partie du même réseau peuvent utiliser leur port interne (ex. : 5000 pour DAB, 3306 pour MySQL)&lt;/li&gt;
&lt;li&gt;Le réseau externe est le &lt;code&gt;proxy&lt;/code&gt; créé précédemment&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="configuration-du-tunnel-cloudflare"&gt;Configuration du tunnel Cloudflare&lt;/h2&gt;
&lt;p&gt;Dans votre tableau de bord Cloudflare, vous devrez configurer le tunnel pour acheminer le trafic vers Traefik. Créez un nom d'hôte public pointant vers &lt;code&gt;http://&amp;lt;ip-locale&amp;gt;:8082&lt;/code&gt;. Utilisez l'adresse IP locale de votre serveur, quelque chose comme "192.168.1.123". Vous pouvez utiliser des caractères génériques comme &lt;code&gt;*.c5m.ca&lt;/code&gt; pour acheminer tous les sous-domaines vers Traefik, qui gérera ensuite le routage selon le nom d'hôte.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;C'est tout ! Une fois que tout est configuré :&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2026/01/cloudflare-traefik.png" alt="diagramme cloudflare traefik homeserver"&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Le tunnel Cloudflare crée une connexion sécurisée de votre domicile vers Cloudflare&lt;/li&gt;
&lt;li&gt;Le trafic arrive via Cloudflare et est routé vers Traefik&lt;/li&gt;
&lt;li&gt;Traefik lit le nom d'hôte et route vers le conteneur approprié&lt;/li&gt;
&lt;li&gt;Chaque service peut être accessible via son propre sous-domaine&lt;/li&gt;
&lt;li&gt;Seuls les conteneurs avec les étiquettes Traefik sont accessibles depuis l'extérieur de mon réseau&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;C'est une configuration simple qui fonctionne très bien pour les projets personnels. Le meilleur aspect est que vous n'avez pas besoin d'exposer de ports sur votre routeur ou de gérer le DNS dynamique, Cloudflare s'occupe de tout ça.&lt;/p&gt;
&lt;p&gt;La prochaine étape sera d'ajouter de l'authentification et de l'autorisation (ex. : en utilisant Keycloak), mais c'est pour un autre billet. Pour l'instant, cela me donne un moyen d'accéder à mes services hébergés à la maison depuis n'importe où, et j'ai pensé que ça pourrait être utile à partager.&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>D'heures à minutes: Construire une IA qui trouve des événements techno pour vous</title>
			<link>/posts/2026-01-05-event-finder.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/Cloudenfrancais-logo3.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2026-01-05-event-finder.html</guid>
			<pubDate>Mon, 05 Jan 2026 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="dheures-a-minutes-une-ia-qui-trouve-des-evenements-techno-pour-vous"&gt;D'heures à minutes: une IA qui trouve des événements techno pour vous&lt;/h1&gt;
&lt;h4 id="tldr"&gt;TL;DR&lt;/h4&gt;
&lt;p&gt;J'ai construit un agent de recherche IA qui navigue réellement le web en direct et trouve des événements techno—aucune boucle de recherche, aucune logique de réessai, aucune hallucination. Posez simplement une question et obtenez un JSON structuré avec les étapes de raisonnement incluses. Le secret? Une API qui gère automatiquement la recherche en plusieurs étapes. Construit avec .NET/Blazor en une fin de semaine. &lt;a href="https://www.youtube.com/watch?v=ML-9SrQm2Dk"&gt;Regardez la video&lt;/a&gt; | &lt;a href="https://link.reka.ai/event-finder-dotnet"&gt;Obtenez le code&lt;/a&gt; | &lt;a href="https://link.reka.ai/free"&gt;Clé API gratuite&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Bonne année! Je voulais partager quelque chose que j'ai récemment présenté à la conférence AI Agents 2025: comment construire des assistants de recherche intelligents qui peuvent chercher sur le web en direct et retourner des résultats structurés et fiables.&lt;/p&gt;
&lt;p&gt;En revenant des vacances, je suis rappelé d'un problème universel: la surcharge d'informations. Que ce soit pour trouver des conférences techno pertinentes, rattraper les nouvelles de l'industrie, ou parcourir des piles de documentation accumulée pendant le congé, nous avons tous besoin d'outils qui peuvent rapidement rechercher et synthétiser l'information pour nous. C'est ce que fait Reka Research—c'est une IA agentique qui navigue le web (ou vos documents privés), répond à des questions complexes, et transforme des heures de recherche en minutes. J'ai construit une démo pratique pour montrer cela en action: un chercheur d'événements qui recherche sur internet en direct les prochaines conférences techno.&lt;/p&gt;
&lt;p&gt;La présentation complète est disponible sur YouTube si vous voulez suivre: &lt;a href="https://www.youtube.com/watch?v=ML-9SrQm2Dk"&gt;How to Build Agentic Web Research Assistants&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="le-probleme-trouver-des-evenements-nest-pas-quune-simple-recherche"&gt;Le problème: Trouver des événements n'est pas qu'une simple recherche&lt;/h2&gt;
&lt;p&gt;Laissez-moi vous dresser le tableau. Vous voulez trouver des conférences techno sur l'IA dans votre région (ou ailleur). Vous avez besoin d'informations spécifiques: le nom de l'événement, les dates de début et de fin, l'emplacement, et surtout, l'URL d'inscription.&lt;/p&gt;
&lt;p&gt;Une simple recherche web ou requête LLM de base ne suffit pas parce que:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Vous pourriez obtenir des informations dépassées&lt;/li&gt;
&lt;li&gt;Le premier résultat de recherche contient rarement tous les détails requis&lt;/li&gt;
&lt;li&gt;Vous devez croiser plusieurs sources&lt;/li&gt;
&lt;li&gt;Sans structure, les données sont difficiles à utiliser dans une application&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C'est là que l'API Research de Reka brille. Elle ne fait pas que chercher—elle raisonne à travers plusieurs étapes, agrège l'information, et retourne des résultats structurés et fondés.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2026/01/events_finder.png" alt="Interface du chercheur d'événements"&gt;&lt;/p&gt;
&lt;h2 id="la-solution-une-recherche-en-plusieurs-etapes-qui-fonctionne-reellement"&gt;La solution: Une recherche en plusieurs étapes qui fonctionne réellement&lt;/h2&gt;
&lt;p&gt;L'innovation centrale ici est l'ancrage en plusieurs étapes. Au lieu de faire une seule requête en espérant pour le mieux, l'API Research agit comme un chercheur humain diligent:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Elle fait une recherche initiale basée sur votre requête&lt;/li&gt;
&lt;li&gt;Vérifie quelle information manque&lt;/li&gt;
&lt;li&gt;Effectue des recherches ciblées additionnelles&lt;/li&gt;
&lt;li&gt;Agrège et valide les données&lt;/li&gt;
&lt;li&gt;Retourne une réponse complète et structurée&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;En tant que développeur, vous envoyez simplement votre question, et l'API gère l'itération complexe. Pas besoin de construire vos propres boucles de recherche ou logique de réessai.&lt;/p&gt;
&lt;h2 id="comment-ca-fonctionne-lexperience-developpeur"&gt;Comment ça fonctionne: L'expérience développeur&lt;/h2&gt;
&lt;p&gt;Voici ce qui m'a le plus surpris: la simplicité. Vous définissez votre structure de données, posez une question, et l'API gère toute l'orchestration de recherche complexe. Aucune logique de réessai, aucune gestion de boucle de recherche.&lt;/p&gt;
&lt;p&gt;La clé est la sortie structurée. Au lieu d'analyser du texte désordonné, vous dites exactement à l'API quel schéma JSON vous voulez:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-csharp"&gt;public class TechEvent
{
    public string? Name { get; set; }
    public DateTime? StartDate { get; set; }
    public DateTime? EndDate { get; set; }
    public string? City { get; set; }
    public string? Country { get; set; }
    public string? Url { get; set; }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ensuite, vous envoyez votre requête avec le schéma, et elle retourne des données parfaitement structurées à chaque fois. L'API utilise un format compatible OpenAI, donc si vous avez travaillé avec l'API de ChatGPT, ça vous semblera instantanément familier.&lt;/p&gt;
&lt;p&gt;La vraie magie? Vous obtenez aussi les étapes de raisonnement en retour—les recherches web réelles qu'elle a effectuées et comment elle est arrivée à la réponse. Parfait pour déboguer et comprendre le processus de pensée de l'agent.&lt;/p&gt;
&lt;p&gt;Je parcours l'implémentation complète, incluant le filtrage de domaine, la recherche géolocalisée, et la gestion des appels de recherche asynchrones dans la &lt;a href="https://www.youtube.com/watch?v=ML-9SrQm2Dk"&gt;vidéo&lt;/a&gt;. Le &lt;a href="https://link.reka.ai/event-finder-dotnet"&gt;code source complet&lt;/a&gt; est sur GitHub si vous voulez approfondir.&lt;/p&gt;
&lt;h2 id="essayez-le-vous-meme"&gt;Essayez-le vous-même&lt;/h2&gt;
&lt;p&gt;Le &lt;a href="https://link.reka.ai/event-finder-dotnet"&gt;code source complet&lt;/a&gt; est sur GitHub. Clonez-le, obtenez une &lt;a href="https://link.reka.ai/free"&gt;clé API gratuite&lt;/a&gt;, et vous l'aurez en fonction en moins de 5 minutes.&lt;/p&gt;
&lt;p&gt;Je suis curieux de voir ce que vous allez construire avec ceci. Des agents de recherche qui surveillent les nouvelles? Des outils de comparaison de produits? Des synthétiseurs de documentation? L'API fonctionne pour n'importe quelle tâche de recherche web. Si vous construisez quelque chose, identifiez-moi—j'aimerais le voir.&lt;/p&gt;
&lt;p&gt;Bonne année! 🎉&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Automatiser le clipping vidéo YouTube avec l'IA et n8n</title>
			<link>/posts/2025-12-18-automatiser-le-clipping-video.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/Cloudenfrancais-logo3.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2025-12-18-automatiser-le-clipping-video.html</guid>
			<pubDate>Thu, 18 Dec 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;h1 id="automatiser-le-clipping-video-youtube-avec-lia-et-n8n"&gt;Automatiser le clipping vidéo YouTube avec l'IA et n8n&lt;/h1&gt;
&lt;p&gt;Vous êtes créateur de contenu ou fan de shorts vidéo? Vous avez sûrement déjà vécu cette situation : une vidéo sort, vous repérez LE moment parfait à clipper, mais quand vous vous mettez au travail, 50 personnes l'ont déjà fait. La course contre la montre, on connaît tous.&lt;/p&gt;
&lt;p&gt;Annie, une collègue passionnée de clipping, vivait ça quotidiennement. Je me suis donc posé la question: pourquoi ne pas laisser l'IA faire ce boulot dès qu'une vidéo est publiée?&lt;/p&gt;
&lt;p&gt;Spoiler : ça fonctionne. Et je vous montre comment reproduire ce système gratuitement.&lt;/p&gt;
&lt;h2 id="le-concept"&gt;Le concept&lt;/h2&gt;
&lt;p&gt;L'idée est simple : un flow surveille votre chaîne YouTube favorite, détecte les nouvelles vidéos, et génère automatiquement des clips optimisés pour les réseaux sociaux. Sans intervention humaine.&lt;/p&gt;
&lt;p&gt;Le système repose sur :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;n8n&lt;/strong&gt; : une plateforme d'automatisation open-source (gratuite)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reka AI&lt;/strong&gt; : leur API Clips qui analyse et découpe les vidéos intelligemment&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Votre boîte mail&lt;/strong&gt; : pour recevoir les clips prêts à poster&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Trois étapes, zéro effort manuel, 100% gratuit.&lt;/p&gt;
&lt;h2 id="larchitecture-technique"&gt;L'architecture technique&lt;/h2&gt;
&lt;p&gt;Le système se compose de deux workflows n8n complémentaires. Pensez-y comme deux employés qui bossent en tandem.&lt;/p&gt;
&lt;h3 id="premier-workflow-le-detecteur"&gt;Premier workflow : Le détecteur&lt;/h3&gt;
&lt;p&gt;&lt;img src="/content/images/2025/12/flow_create-clip-job.png" alt="n8n workflow creation"&gt;&lt;/p&gt;
&lt;p&gt;Son job? Être à l'affût. Il scrute le flux RSS d'une chaîne YouTube. Nouvelle vidéo détectée? Il déclenche le processus :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Extraction de l'URL&lt;/li&gt;
&lt;li&gt;Appel à l'API Reka avec vos instructions personnalisées&lt;/li&gt;
&lt;li&gt;Récupération d'un identifiant de tâche&lt;/li&gt;
&lt;li&gt;Stockage des infos dans une table n8n&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;La beauté de cette solution? Tout est paramétrable. Format vertical pour Instagram et TikTok, horizontal pour YouTube Shorts, carré pour d'autres plateformes. Sous-titres activés ou non. Durée du clip modulable de 0 à 30 secondes. Vous contrôlez tout via une simple
Vous pouvez personnaliser la façon dont les clips sont créés. Vous voulez des vidéos verticales pour TikTok? C'est fait. Besoin de sous-titres? Pas de problème. Vous pouvez définir la durée du clip entre 0 et 30 secondes. Tout est dans la configuration JSON.
Exemple de configuration :&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-json"&gt;{
  "video_urls": ["{{ $json.link }}"],
  "prompt": "Identifie et extrait les passages les plus percutants de cette vidéo",
  "generation_config": {
    "template": "moments",
    "num_generations": 1,
    "min_duration_seconds": 0,
    "max_duration_seconds": 30
  },
  "rendering_config": {
    "subtitles": true,
    "aspect_ratio": "9:16"
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="second-workflow-le-verificateur"&gt;Second workflow : Le vérificateur&lt;/h3&gt;
&lt;p&gt;&lt;img src="/content/images/2025/12/flow_check-status.png" alt="n8n workflow status"&gt;&lt;/p&gt;
&lt;p&gt;Pendant que l'IA travaille (l'analyse peut prendre de quelques minutes à plus longtemps selon la vidéo), ce workflow vérifie l'avancement :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Consultation régulière de la base de données pour les tâches en cours&lt;/li&gt;
&lt;li&gt;Interrogation de l'API : "C'est prêt?"&lt;/li&gt;
&lt;li&gt;Envoi d'un email dès qu'un clip est disponible&lt;/li&gt;
&lt;li&gt;Mise à jour du statut pour éviter les vérifications inutiles&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Personnellement, je lance cette vérification toutes les 20 minutes. Inutile de harceler l'API toutes les 2 minutes—la patience est une vertu, même en automatisation.&lt;/p&gt;
&lt;h2 id="mise-en-place-pas-a-pas"&gt;Mise en place pas à pas&lt;/h2&gt;
&lt;p&gt;La configuration prend moins de 10 minutes. Vraiment. Annie l'a testée en direct lors de notre session d'enregistrement, et tout était opérationnel avant même qu'on finisse nos cafés.&lt;/p&gt;
&lt;h3 id="premiere-etape-preparez-votre-base-de-donnees"&gt;Première étape : Préparez votre base de données&lt;/h3&gt;
&lt;p&gt;Direction n8n, créez une table de données. Petit piège à éviter : n'utilisez pas "videos" comme nom (vous me remercierez plus tard). Optez pour "clip_jobs", "youtube_reels" ou autre chose de descriptif.&lt;/p&gt;
&lt;p&gt;Structure de la table - 4 colonnes (format texte) :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;video_title&lt;/code&gt; : titre de la vidéo YouTube&lt;/li&gt;
&lt;li&gt;&lt;code&gt;video_url&lt;/code&gt; : lien direct&lt;/li&gt;
&lt;li&gt;&lt;code&gt;job_id&lt;/code&gt; : identifiant fourni par Reka&lt;/li&gt;
&lt;li&gt;&lt;code&gt;job_status&lt;/code&gt; : état d'avancement queued, processing, completed..&lt;/li&gt;
&lt;li&gt;&lt;code&gt;job_id&lt;/code&gt; - L'ID que Reka nous donne pour suivre le clip&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="deuxieme-etape-import-des-workflows"&gt;Deuxième étape : Import des workflows&lt;/h3&gt;
&lt;p&gt;Récupérez les deux templates JSON sur GitHub, puis importez-les dans n8n. Normal qu'ils affichent des erreurs initialement—ils attendent d'être paramétrés.&lt;/p&gt;
&lt;h3 id="troisieme-etape-parametrage-du-detecteur"&gt;Troisième étape : Paramétrage du détecteur&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Flux RSS&lt;/strong&gt; : Indiquez l'ID de la chaîne YouTube à surveiller (trouvable dans l'URL de n'importe quelle chaîne).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authentification API&lt;/strong&gt; : Rendez-vous sur &lt;a href="https://link.reka.ai/free"&gt;platform.reka.ai&lt;/a&gt; pour obtenir votre clé gratuite. Collez-la dans le champ Bearer Auth. Nommez-la explicitement pour vous y retrouver.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Instructions pour l'IA&lt;/strong&gt; : Définissez vos critères de clipping. Par défaut, vous obtenez du 9:16 (format vertical) de 30 secondes max avec sous-titres. Mais modifiez selon vos besoins :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Rédigez votre propre prompt&lt;/li&gt;
&lt;li&gt;Ajustez la durée minimale et maximale&lt;/li&gt;
&lt;li&gt;Choisissez le format (1:1, 9:16, 16:9...)&lt;/li&gt;
&lt;li&gt;Activez/désactivez les sous-titres&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Connexion base de données&lt;/strong&gt; : Liez ce workflow à votre table créée précédemment.&lt;/p&gt;
&lt;h3 id="quatrieme-etape-parametrage-du-verificateur"&gt;Quatrième étape : Paramétrage du vérificateur&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Déclencheur&lt;/strong&gt; : Testez d'abord manuellement. Une fois validé, programmez des vérifications automatiques (intervalle recommandé : 15-30 minutes).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Authentification&lt;/strong&gt; : Même clé API Reka que précédemment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Notification&lt;/strong&gt; : Configurez le nœud email avec votre adresse. Le template par défaut est efficace, mais personnalisez si vous le souhaitez.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Base de données&lt;/strong&gt; : Vérifiez que tous les nœuds référencent bien votre tablesi vous voulez, mais celui par défaut fonctionne très bien.&lt;/p&gt;
&lt;h2 id="demonstration-video"&gt;Démonstration vidéo&lt;/h2&gt;
&lt;p&gt;Toute la procédure est filmée avec Annie qui configure le système en temps réel. Chaque clic, chaque paramètre, même nos petites galères—tout est dedans. C'est brut, c'est authentique, et ça montre que vraiment, n'importe qui peut le faire. La vidéo est en anglais, n'hésitez pas à partager vos questions en commantaires, s'il y a de l'intérêt je ferai une version française.&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/6YV6K94m_FA?si=USYShbAkB-upT12I" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;h2 id="pour-conclure"&gt;Pour conclure&lt;/h2&gt;
&lt;p&gt;Cet outil fonctionne à merveille, que vous soyez un passionné de clipping ou un créateur de contenu qui veut générer des clips pour sa propre chaîne. Une fois configuré, ça roule tout seul. Une nouvelle vidéo sort à 3h du matin? Votre clip est déjà en traitement. Vous vous réveillez avec un lien de téléchargement dans votre boîte de réception.&lt;/p&gt;
&lt;p&gt;C'est open source et gratuit. Prenez-le, personnalisez-le, faites-en votre affaire. Et si vous avez des améliorations ou des idées, j'adorerais en entendre parler. Partagez vos mises à jour sur &lt;a href="https://link.reka.ai/n8n-clip"&gt;GitHub&lt;/a&gt; ou rejoignez la conversation sur le &lt;a href="https://link.reka.ai/discord"&gt;Discord de la communauté Reka&lt;/a&gt; .Ou contactez-moi directement, je réponds toujours.&lt;/p&gt;
&lt;h2 id="ressources-et-liens-utiles"&gt;Ressources et liens utiles&lt;/h2&gt;
&lt;p&gt;Pour démarrer immédiatement :&lt;/p&gt;
&lt;p&gt;🔗 &lt;a href="https://link.reka.ai/n8n-clip"&gt;Templates n8n sur GitHub&lt;/a&gt;&lt;br&gt;
🔗 &lt;a href="https://link.reka.ai/free"&gt;Clé API Reka (gratuite)&lt;/a&gt;&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Pourquoi votre badge de "Code Coverage" .NET affiche "Unknown" dans GitLab (et comment le réparer)</title>
			<link>/posts/2025-07-17-dotnet-code-coverage-on-gitlab.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/Cloudenfrancais-logo3.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2025-07-17-dotnet-code-coverage-on-gitlab.html</guid>
			<pubDate>Thu, 17 Jul 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Dans un récent &lt;a href="https://www.cloudenfrancais.com/posts/2025-07-07-comment-avoir-un-cicd-pour-aspire-sur-gitlab.html"&gt;blog post&lt;/a&gt;, j'ai partagé comment configurer un pipeline CI/CD pour un projet .NET Aspire sur GitLab. Le pipeline inclut des tests unitaires, une analyse de sécurité, et la détection de secrets, et si l'un de ces éléments échoue, le pipeline échouerait. Super, mais qu'en est-il de la couverture de code (Code Coverage) pour les tests unitaires ? Le pipeline incluait des commandes de couverture de code, mais la couverture n'était pas visible dans l'interface GitLab. Réglons ça.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2025/07/coverage-unknown.png" alt="Badge sur GitLab montrant couverture inconnue"&gt;&lt;/p&gt;
&lt;h2 id="le-probleme"&gt;Le problème&lt;/h2&gt;
&lt;p&gt;Une chose que j'ai d'abord pensé c'est que le regex utilisé pour extraire la couverture était incorrect. Le regex utilisé dans le pipeline était:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;coverage: '/Total\s*\|\s*(\d+(?:\.\d+)?)%/'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ce regex venait directement de la &lt;a href="https://docs.gitlab.com/ci/testing/code_coverage/#coverage-regex-patterns"&gt;documentation GitLab&lt;/a&gt;, alors je pensais qu'il devrait fonctionner correctement. Cependant, la couverture n'était toujours pas visible dans l'interface GitLab.&lt;/p&gt;
&lt;p&gt;Alors avec l'aide de GitHub Copilot, j'ai écrit quelques commandes pour valider:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Que le &lt;code&gt;coverage.cobertura.xml&lt;/code&gt; était dans un emplacement cohérent (au lieu d'être dans un dossier avec un nom GUID)&lt;/li&gt;
&lt;li&gt;Que le fichier &lt;code&gt;coverage.cobertura.xml&lt;/code&gt; était dans un format valide&lt;/li&gt;
&lt;li&gt;Ce que le regex cherchait exactement&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Tout était correct, alors pourquoi la couverture n'était-elle pas visible?&lt;/p&gt;
&lt;h2 id="la-solution"&gt;La solution&lt;/h2&gt;
&lt;p&gt;Il s'avère que la commande &lt;code&gt;coverage&lt;/code&gt; avec l'expression regex scanne le output console et non le fichier &lt;code&gt;coverage.cobertura.xml&lt;/code&gt;. Aha ! Une solution était d'installer d'autre &lt;code&gt;dotnet-tools&lt;/code&gt; pour changer où les résultats de test étaient persistés; vers la console au lieu du fichier XML, mais j'ai préféré garder l'environnement .NET inchangé.&lt;/p&gt;
&lt;p&gt;La solution que j'ai fini par implémenter était d'exécuter une commande &lt;code&gt;grep&lt;/code&gt; pour extraire la couverture du fichier &lt;code&gt;coverage.cobertura.xml&lt;/code&gt; et ensuite l'afficher dans la console. Voici à quoi ça ressemble:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;- COVERAGE=$(grep -o 'line-rate="[0-9.]*"' TestResults/coverage.cobertura.xml | head -1 | grep -o '[0-9.]*' | awk '{printf "%.1f", $1*100}')
- echo "Total | ${COVERAGE}%"
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="resultats"&gt;Résultats&lt;/h2&gt;
&lt;p&gt;Et maintenant quand le pipeline s'exécute, la couverture est visible dans le pipeline GitLab!&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2025/07/coverage-pipeline.png" alt="Couverture visible dans le pipeline GitLab"&gt;&lt;/p&gt;
&lt;p&gt;Et le badge est mis à jour pour afficher le pourcentage de couverture.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2025/07/coverage-works.png" alt="Badge de couverture montrant le pourcentage"&gt;&lt;/p&gt;
&lt;h2 id="configuration-complete"&gt;Configuration complète&lt;/h2&gt;
&lt;p&gt;Voici la configuration complète du job de test. Bien sûr, le fichier &lt;a href="https://gitlab.com/cloud5mins/aspire-template"&gt;.gitlab-ci.yml&lt;/a&gt; complet est disponible dans le dépôt GitLab.&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-yaml"&gt;test:
  stage: test
  image: mcr.microsoft.com/dotnet/sdk:9.0
  &amp;lt;&amp;lt;: *dotnet_cache
  dependencies:
    - build
  script:
    - dotnet test $SOLUTION_FILE --configuration Release --logger "junit;LogFilePath=$CI_PROJECT_DIR/TestResults/test-results.xml" --logger "console;verbosity=detailed" --collect:"XPlat Code Coverage" --results-directory $CI_PROJECT_DIR/TestResults
    - find TestResults -name "coverage.cobertura.xml" -exec cp {} TestResults/coverage.cobertura.xml \;
    - COVERAGE=$(grep -o 'line-rate="[0-9.]*"' TestResults/coverage.cobertura.xml | head -1 | grep -o '[0-9.]*' | awk '{printf "%.1f", $1*100}')
    - echo "Total | ${COVERAGE}%"
  artifacts:
    when: always
    reports:
      junit: "TestResults/test-results.xml"
      coverage_report:
        coverage_format: cobertura
        path: "TestResults/coverage.cobertura.xml"
    paths:
      - TestResults/
    expire_in: 1 week
  coverage: '/Total\s*\|\s*(\d+(?:\.\d+)?)%/'
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;J'espère que ce post aidera d'autres personnes à sauver du temps lors de la configuration de la couverture de code pour leurs projets .NET sur GitLab. L'idée clé c'est que le regex de couverture de GitLab fonctionne sur la sortie console, pas sur les fichiers (XML ou autres formats).&lt;/p&gt;
&lt;p&gt;Si vous avez des questions ou des suggestions, n'hésitez pas à me contacter !&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Comment avoir un CI/CD GitLab pour un projet .NET Aspire</title>
			<link>/posts/2025-07-07-comment-avoir-un-cicd-pour-aspire-sur-gitlab.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/2025/07/aspire_template.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2025-07-07-comment-avoir-un-cicd-pour-aspire-sur-gitlab.html</guid>
			<pubDate>Mon, 07 Jul 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Obtenir un pipeline CI/CD complet pour votre solution .NET Aspire n'a pas besoin d'être compliqué. J'ai créé un template qui vous donne tout ce dont vous avez besoin pour commencer en quelques minutes.&lt;/p&gt;
&lt;h2 id="partie-1-le-template-pret-a-utiliser"&gt;Partie 1 : Le template prêt à utiliser&lt;/h2&gt;
&lt;p&gt;J'ai créé un template .NET Aspire qui vient avec tout configuré et prêt à utiliser. Voici ce que vous obtenez :&lt;/p&gt;
&lt;h3 id="ce-qui-est-inclus"&gt;Ce qui est inclus&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Un projet .NET Aspire Starter classique (API et frontend)&lt;/li&gt;
&lt;li&gt;Tests unitaires utilisant xUnit (facilement adaptable à d'autres frameworks de test)&lt;/li&gt;
&lt;li&gt;Configuration complète du pipeline &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Analyse de sécurité et détection de secrets&lt;/li&gt;
&lt;li&gt;Toute la documentation dont vous avez besoin&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="ce-que-fait-le-pipeline"&gt;Ce que fait le pipeline&lt;/h3&gt;
&lt;p&gt;Le pipeline exécute deux jobs principaux automatiquement :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Build&lt;/strong&gt; : Compile votre code&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Test&lt;/strong&gt; : Exécute tous les tests unitaires, analyse les vulnérabilités, et vérifie les secrets accidentellement committés (clés API, mots de passe, etc.)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Vous pouvez voir tous les résultats de tests directement dans l'interface de GitLab, ce qui facilite le suivi de la santé de votre projet.&lt;/p&gt;
&lt;h3 id="comment-commencer"&gt;Comment commencer&lt;/h3&gt;
&lt;p&gt;C'est simple :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Clonez le repository template : &lt;a href="https://gitlab.com/cloud5mins/aspire-template"&gt;cloud5mins/aspire-template&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Remplacez le projet exemple avec votre propre code .NET Aspire&lt;/li&gt;
&lt;li&gt;Poussez vers votre repository GitLab&lt;/li&gt;
&lt;li&gt;Regardez votre pipeline CI/CD s'exécuter automatiquement&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;C'est tout ! Vous obtenez immédiatement des builds automatisés, des tests, et de l'analyse de sécurité.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Conseil de pro&lt;/strong&gt; : Le meilleur moment pour configurer le CI/CD c'est quand vous commencez tout juste votre projet parce que tout est encore simple.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="partie-2-construire-le-template-avec-gitlab-duo"&gt;Partie 2 : Construire le template avec GitLab Duo&lt;/h2&gt;
&lt;p&gt;Maintenant laissez-moi partager mon expérience de création de ce template en utilisant l'assistant IA de GitLab, &lt;a href="https://about.gitlab.com/gitlab-duo/"&gt;GitLab Duo&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="commencer-simple-grandir-intelligemment"&gt;Commencer simple, grandir intelligemment&lt;/h3&gt;
&lt;p&gt;Je n'ai pas construit ce pipeline complexe d'un coup. J'ai commencé avec quelque chose de très basique et j'ai utilisé GitLab Duo pour graduellement ajouter des fonctionnalités. L'IA m'a aidé à :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Ajouter la détection de secrets quand j'ai demandé : "Comment puis-je analyser les secrets accidentellement committés ?"&lt;/li&gt;
&lt;li&gt;Corriger les problèmes d'exécution de tests quand mes tests unitaires ne roulaient pas correctement&lt;/li&gt;
&lt;li&gt;Optimiser la structure du pipeline pour une meilleure performance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src="/content/images/2025/07/duo_sast.png" alt="capture d'écran dans VSCode utilisant GitLab Duo pour changer l'emplacement par défaut du job SAST"&gt;&lt;/p&gt;
&lt;h3 id="travailler-avec-gitlab-dans-vs-code"&gt;Travailler avec GitLab dans VS Code&lt;/h3&gt;
&lt;p&gt;Bien que vous puissiez éditer les fichiers &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; directement dans l'interface web de GitLab, je préfère VS Code. Voici ma configuration :&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Installez l'extension officielle &lt;strong&gt;GitLab&lt;/strong&gt; du marketplace VS Code&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Une fois connecté, cette extension vous donne :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accès direct aux issues GitLab et items de travail&lt;/li&gt;
&lt;li&gt;Chat alimenté par l'IA avec GitLab Duo&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="gitlab-duo-en-action"&gt;GitLab Duo en action&lt;/h3&gt;
&lt;p&gt;GitLab Duo est devenu mon partenaire de programmation en paire. Voici comment je l'ai utilisé :&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Comprendre le code&lt;/strong&gt; : Je pouvais taper &lt;code&gt;/explain&lt;/code&gt; et demander à Duo d'expliquer ce que n'importe quelle partie de ma configuration de pipeline fait en surlignant cette section.&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2025/07/duo_explain.png" alt="capture d'écran dans VSCode utilisant GitLab Duo pour expliquer une partie du code"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Résoudre des problèmes&lt;/strong&gt; : Quand la solution ne compilait pas, j'ai décrit le problème à Duo et j'ai obtenu des suggestions spécifiques. Par exemple, ça m'a aidé à réaliser que certains projets n'étaient pas en .NET 9 parce que dotnet build nécessitait la charge de travail Aspire. Je pouvais soit garder mon projet en .NET 8 et ajouter une instruction before_script pour installer la charge de travail, soit faire la mise à niveau vers .NET 9; j'ai choisi la dernière option.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ajouter des fonctionnalités&lt;/strong&gt; : J'ai commencé avec juste build et test, puis j'ai demandé de façon incrémentale à Duo de m'aider à ajouter l'analyse de sécurité, la détection de secrets, et une meilleure gestion d'erreurs.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ajouter du contexte&lt;/strong&gt; : Utiliser &lt;code&gt;/include&lt;/code&gt; pour ajouter le fichier projet ou le fichier &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; en posant des questions a aidé Duo à mieux comprendre le contexte.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Apprendre plus avec la doc&lt;/strong&gt; : Durant mon parcours, je savais que Duo n'inventait pas des affaires puisqu'il référençait la documentation. Je pouvais continuer mon apprentissage là-bas et lire plus d'exemples de comment &lt;code&gt;before_script&lt;/code&gt; est utilisé dans différents contextes.&lt;/p&gt;
&lt;h3 id="lexperience-de-developpement-assistee-par-ia"&gt;L'expérience de développement assistée par IA&lt;/h3&gt;
&lt;p&gt;Ce qui m'a le plus impressionné c'est comment GitLab Duo m'a aidé à apprendre en construisant. Au lieu de juste copier des configurations de la documentation, chaque conversation m'a appris quelque chose de nouveau sur les meilleures pratiques GitLab CI/CD.&lt;/p&gt;
&lt;h2 id="regardez-la-video-en-anglais"&gt;Regardez la vidéo (en anglais)&lt;/h2&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/COWLi_OxOh4?si=29I5wD_FoQ4e2MCy" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen=""&gt;&lt;/iframe&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Je pense que ce template peut être utile pour quiconque commence un projet .NET Aspire. Prêt à l'essayer ? Clonez le template à &lt;a href="https://gitlab.com/cloud5mins/aspire-template"&gt;cloud5mins/aspire-template&lt;/a&gt; et commencez à construire en toute confiance.&lt;/p&gt;
&lt;p&gt;Que vous soyez nouveau à .NET Aspire ou au CI/CD, ce template vous donne une bonne fondation. Et si vous voulez le personnaliser davantage, GitLab Duo est là pour vous aider à comprendre et modifier la configuration.&lt;/p&gt;
&lt;p&gt;Si vous pensez qu'on devrait ajouter plus de fonctionnalités ou améliorer le template, n'hésitez pas à ouvrir une issue dans le repository. Vos commentaires sont toujours les bienvenus !&lt;/p&gt;
&lt;p&gt;&lt;img src="/content/images/2025/07/aspire_template.png" alt="Capture d'écran du projet template Aspire sur GitLab"&gt;&lt;/p&gt;
&lt;p&gt;Merci à &lt;a href="https://bsky.app/profile/davidfowl.com/post/3ltiwbulr222r"&gt;David Fowler&lt;/a&gt; pour son feedback!&lt;/p&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
		<item>
			<title>Comment convertir du code avec GitHub Copilot, l'IA peut-elle vraiment aider ?</title>
			<link>/posts/2025-07-01-convert-code-with-github-copilot-fr.html</link>
			<description>Parlons du «cloud computing» en français</description>
			<enclosure url="/content/images/Cloudenfrancais-logo3.png" length="0" type="image" />
			<guid isPermaLink="false">/posts/2025-07-01-convert-code-with-github-copilot-fr.html</guid>
			<pubDate>Tue, 01 Jul 2025 00:00:00 GMT</pubDate>
			<content:encoded>&lt;p&gt;Récemment, quelqu'un m'a posé une question intéressante: &amp;quot;GitHub Copilot ou l'IA peuvent-ils m'aider à convertir une application d'un langage à un autre ?&amp;quot; Ma réponse était un oui catégorique! L'IA peut non seulement aider à écrire du code dans un nouveau langage, mais elle peut aussi améliorer la collaboration en équipe et combler le manque d'expérience des développeurs qui connaissent différents langages de programmation.&lt;/p&gt;
&lt;h2 id="configuration-de-lenvironnement"&gt;Configuration de l'environnement&lt;/h2&gt;
&lt;p&gt;Pour démontrer cette capacité, j'ai décidé de convertir une application COBOL en Java. Un cas de test parfait puisque je ne connais bien aucun de ces deux langages, ce qui signifie que j'avais vraiment besoin de GitHub Copilot pour faire le gros du travail. Tout le code est disponible sur &lt;a href="https://github.com/FBoucher/hello_business/tree/demo"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;La première étape était de configurer un environnement de développement approprié. J'ai utilisé un &lt;a href="https://github.com/FBoucher/hello_business/tree/demo/.devcontainer"&gt;dev container&lt;/a&gt; et j'ai demandé à Copilot de m'aider à le construire. J'ai aussi demandé des recommandations sur les &lt;a href="https://marketplace.visualstudio.com/items?itemName=vscjava.vscode-java-pack"&gt;meilleures extensions VS Code pour le développement Java&lt;/a&gt;. En quelques minutes seulement, j'avais un environnement entièrement configuré et prêt pour le développement Java.&lt;/p&gt;
&lt;h2 id="choisir-le-bon-agent-copilot"&gt;Choisir le bon agent Copilot&lt;/h2&gt;
&lt;p&gt;Lorsque vous travaillez avec GitHub Copilot pour la conversion de code, vous avez différents mode parmi lesquels choisir :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Ask&lt;/strong&gt; : Excellent pour les questions générales (comme demander des extensions Java)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Edit&lt;/strong&gt; : Parfait pour l'édition simple de documents (comme modifier le code généré)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agent&lt;/strong&gt; : Le plus puissant pour les tâches complexes impliquant plusieurs fichiers, imports et changements structurels&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pour les projets de conversion de code, l'Agent est votre meilleur ami. Il peut examiner différents fichiers sources, comprendre la structure du projet, éditer le code et même créer de nouveaux fichiers pour vous.&lt;/p&gt;
&lt;h2 id="le-processus-de-conversion"&gt;Le processus de conversion&lt;/h2&gt;
&lt;p&gt;J'ai utilisé Claude 3.5 Sonnet pour cette conversion. Voici la simple instruction que j'ai utilisée :&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;Convertis cette application COBOL hello business en Java&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Copilot n'a pas seulement converti le code, il a aussi fourni des informations détaillées sur comment exécuter l'application Java, ce qui était inestimable puisque je n'avais aucune expérience en Java.&lt;/p&gt;
&lt;p&gt;Les résultats variaient selon le modèle d'IA utilisé (Claude, GPT, Gemini, etc.), mais la fonctionnalité de base restait cohérente à travers différentes tentatives. Comme l'application originale était simple, je l'ai convertie plusieurs fois en utilisant différentes instructions et modèles pour tester la cohérence. Parfois, il générait un seul fichier, d'autres fois il créait plusieurs fichiers : une application principale et une classe Employee (qui n'était pas dans ma version COBOL originale). Parfois, il mettait à jour le &lt;code&gt;Makefile&lt;/code&gt; pour permettre la compilation et l'exécution en utilisant &lt;code&gt;make&lt;/code&gt;, tandis que d'autres fois il fournissait des instructions pour utiliser directement les commandes &lt;code&gt;javac&lt;/code&gt; et &lt;code&gt;java&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Cette variabilité est attendue avec l'IA générative—les résultats diffèrent entre les exécutions, mais la fonctionnalité de base reste fiable.&lt;/p&gt;
&lt;h2 id="defis-du-monde-reel"&gt;Défis du monde réel&lt;/h2&gt;
&lt;p&gt;Bien sûr, la conversion n'était pas parfaite du premier coup. Par exemple, j'ai rencontré des erreurs d'exécution lors du lancement de l'application. Le problème était avec le format des données—le fichier original utilisait un format de fichier plat avec des enregistrements de longueur fixe (19 caractères par enregistrement) et sans saut de ligne.&lt;/p&gt;
&lt;p&gt;Je suis retourné vers Copilot, j'ai mis en évidence le message d'erreur du terminal, et j'ai fourni un contexte supplémentaire sur le format d'enregistrement de 19 caractères. Cette approche itérative est la clé du succès de la conversion assistée par IA.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;Ça ne fonctionne pas comme prévu, vérifie l'erreur dans #terminalSelection. Les enregistrements ont une longueur fixe de 19 caractères sans saut de ligne. Ajuste le code pour gérer ce format&amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="les-resultats"&gt;Les résultats&lt;/h2&gt;
&lt;p&gt;Après les améliorations itératives, mon application Java a réussi à :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Compiler sans erreurs&lt;/li&gt;
&lt;li&gt;Traiter tous les enregistrements d'employés&lt;/li&gt;
&lt;li&gt;Générer un rapport avec les données des employés&lt;/li&gt;
&lt;li&gt;Calculer le salaire total (un ajout sympa qui n'était pas dans l'original)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bien que le format de sortie ne soit pas identique à la version COBOL originale (zéros de tête manquants, espacement de ligne différent), la fonctionnalité de base était préservée.&lt;/p&gt;
&lt;h2 id="demonstration-video"&gt;Démonstration vidéo&lt;/h2&gt;
&lt;p&gt;Regardez le processus de conversion complet en action (vidéo en anglais) :&lt;/p&gt;
&lt;iframe width="560" height="315" src="https://www.youtube.com/embed/i5RdOPA-waQ?si=wjyvYF_HQ44MMIz3" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen&gt;&lt;/iframe&gt;
&lt;h2 id="meilleures-pratiques-pour-la-conversion-de-code-assistee-par-ia"&gt;Meilleures pratiques pour la conversion de code assistée par IA&lt;/h2&gt;
&lt;p&gt;Basé sur cette expérience, voici mes recommandations :&lt;/p&gt;
&lt;h3 id="commencer-par-de-petits-morceaux"&gt;1. Commencer par de petits morceaux&lt;/h3&gt;
&lt;p&gt;N'essayez pas de convertir des milliers de lignes d'un coup. Divisez votre conversion en modules ou fonctions gérables.&lt;/p&gt;
&lt;h3 id="etablir-des-standards-de-projet"&gt;2. Établir des standards de projet&lt;/h3&gt;
&lt;p&gt;Considérez créer un dossier &lt;code&gt;.github&lt;/code&gt; à la racine de votre projet avec un fichier &lt;code&gt;instructions.md&lt;/code&gt; contenant :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Les meilleures pratiques pour votre langage cible&lt;/li&gt;
&lt;li&gt;Les modèles et outils à utiliser&lt;/li&gt;
&lt;li&gt;Les versions et frameworks spécifiques&lt;/li&gt;
&lt;li&gt;Les standards d'entreprise à suivre&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="rester-implique-dans-le-processus"&gt;3. Rester impliqué dans le processus&lt;/h3&gt;
&lt;p&gt;Vous n'êtes pas seulement un spectateur - vous êtes un participant actif. Révisez les changements, testez la sortie, et fournissez des commentaires quand les choses ne fonctionnent pas comme prévu.&lt;/p&gt;
&lt;h3 id="iterer-et-ameliorer"&gt;4. Itérer et améliorer&lt;/h3&gt;
&lt;p&gt;N'attendez pas la perfection du premier coup. Dans mon cas, l'application convertie fonctionnait mais produisait un formatage de sortie légèrement différent. C'est normal et attendu, après tout vous convertissez entre deux langages différents avec des conventions et styles différents.&lt;/p&gt;
&lt;h2 id="lia-peut-elle-vraiment-aider-avec-la-conversion-de-code"&gt;L'IA peut-elle vraiment aider avec la conversion de code ?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Absolument, oui !&lt;/strong&gt; GitHub Copilot peut significativement :&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Accélérer le processus de conversion&lt;/li&gt;
&lt;li&gt;Aider avec la syntaxe et les modèles spécifiques au langage&lt;/li&gt;
&lt;li&gt;Fournir des conseils sur l'exécution et la compilation du langage cible&lt;/li&gt;
&lt;li&gt;Combler les lacunes de connaissances entre les membres de l'équipe&lt;/li&gt;
&lt;li&gt;Générer des fichiers de support et de la documentation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Cependant, rappelez-vous que c'est de l'IA générative, les résultats varieront entre les exécutions, et vous ne devriez pas vous attendre à une sortie identique à chaque fois.&lt;/p&gt;
&lt;h2 id="reflexions-finales"&gt;Réflexions finales&lt;/h2&gt;
&lt;p&gt;GitHub Copilot est définitivement un outil dont vous avez besoin dans votre boîte à outils pour les projets de conversion. Il ne remplacera pas le besoin de supervision humaine et de tests, mais il accélérera dramatiquement le processus et aidera les équipes à collaborer plus efficacement à travers différents langages de programmation.&lt;/p&gt;
&lt;p&gt;La clé est d'aborder cela comme un processus collaboratif où l'IA fait le gros du travail pendant que vous fournissez des conseils, du contexte et l'assurance qualité. Commencez petit, itérez souvent, et n'ayez pas peur de demander des clarifications ou des corrections quand la sortie n'est pas tout à fait correcte.&lt;/p&gt;
&lt;p&gt;Avez-vous essayé d'utiliser l'IA pour la conversion de code ? J'aimerais entendre parler de vos expériences dans les commentaires ci-dessous !&lt;/p&gt;
&lt;h4 id="references"&gt;Références&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://code.visualstudio.com/docs/devcontainers/tutorial"&gt;Tutoriel Dev Containers&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/copilot/how-tos/custom-instructions/adding-repository-custom-instructions-for-github-copilot"&gt;Ajouter des instructions personnalisées de dépôt pour GitHub Copilot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded>
			<comments xmlns="http://purl.org/rss/1.0/modules/slash/">0</comments>
		</item>
	</channel>
</rss>