Loupe

Azure Pipelines : Agent de Build avec Azure Container Instances - partie 3 : Agent as a Service

Aller jusqu’à “l’agent de build à la demande”

Dans les articles précédents, nous avons vu comment ajouter en quelques minutes un agent de build en utilisant Azure Container Instances, ainsi que comment utiliser notre propre image de conteneur personnalisé.

Dans cet article, voyons voir comment créer un agent de build “à la volée” lorsqu’une Build est déclenchée, et comment le détruire une fois la build terminée, le tout en utilisant Azure Container Instances, Azure Functions ainsi que les agentless phase d’Azure Pipelines.

Workflow de l’agent de build à la demande

Pour ceux déjà familiers d'Azure Pipelines, la capture d’écran suivante illustre plus facilement que des mots le concept de cet article :

01-build-agent-on-demand-using-agentless-job-and-azure-function.png

Les points importants de cette image sont qu’il y a 3 phases :

  • La première et la dernière phase étant “Run on server” (exécutées sur le serveur), également appelées “agentless phases” (phases sans agent) car elles sont exécutée directement depuis Azure DevOps et ne nécessitent pas d’agent de build. Elles vont permettre d’appeler une Azure Function, qui sera elle même en charge de créer et démarrer / détruire nos agents de build custom hébergés sur Azure Container Instances.
  • La seconde phase comporte notre workflow de build classique : une compilation d’application .NET par exemple.

Cela signifie qu’en suivant ce workflow, nous allons avoir notre propre “Build agent as a Service”, en payant uniquement à la build déclenchée, tout en étant capables de se mettre à l’échelle jusqu’à une limite correspondante au nombre de parallel jobs disponibles !

Mise en place d’une Azure Function chargée de créer / supprimer une Azure Container Instance

Vous pouvez créer une Azure Function directement depuis le portail Azure en suivant la documentation officielle qui se révèle être l’un des moyens les plus simples et faciles de démarrer.

Ensuite, pour être capable d’interagir avec Azure Container Instances, nous pouvons utiliser le package NuGet Microsoft.Azure.Management.Fluent et ensuite modifier le fichier run.csx créé par défaut avec les lignes suivantes :

#r "Newtonsoft.Json"

using System.Net;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Primitives;
using Newtonsoft.Json;
using Microsoft.Azure.Management.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent;
using Microsoft.Azure.Management.ResourceManager.Fluent.Authentication;
using System;
using System.Collections.Generic;

public static async Task<IActionResult> Run(HttpRequest req, ILogger log)
{
    log.LogInformation("C# HTTP trigger function processed a request.");

    string name = req.Query["name"];

    string requestBody = await new StreamReader(req.Body).ReadToEndAsync();
    dynamic data = JsonConvert.DeserializeObject(requestBody);
    name = name ?? data?.name;

    // Azure Settings
    var tenantId = "MY_TENANT_ID";
    var clientId = "MY_CLIENT_ID";
    var clientSecret = "MY_CLIENT_SECRET";
    var subscriptionId = "MY_SUBSCRIPTION_ID";
    var resourceGroup = "MY_RESOURCE_GROUP";
    var agentName = name;

    // Azure DevOps settings
    var imageName = "vfabing/azure-pipelines-agent-dotnet-core-sdk:latest";
    var envConfig = new Dictionary<string, string> {
        { "AZP_URL", "https://dev.azure.com/MY_AZUREDEVOPS_ACCOUNT" },
        { "AZP_TOKEN", "MY_PERSONAL_ACCESS_TOKEN" },
        { "AZP_AGENT_NAME", $"{agentName}" },
    };

    var sp = new ServicePrincipalLoginInformation { ClientId = clientId, ClientSecret = clientSecret };
    var azure = Azure.Authenticate(new AzureCredentials(sp, tenantId, AzureEnvironment.AzureGlobalCloud)).WithSubscription(subscriptionId);
    var rg = azure.ResourceGroups.GetByName(resourceGroup);

    // Azure Container Instance Creation
    new Thread(() => azure.ContainerGroups.Define(agentName)
        .WithRegion(rg.RegionName)
        .WithExistingResourceGroup(rg)
        .WithLinux()
        .WithPublicImageRegistryOnly()
        .WithoutVolume()
        .DefineContainerInstance(agentName)
            .WithImage(imageName)
            .WithoutPorts()
            .WithEnvironmentVariables(envConfig)
            .Attach()
        .Create()).Start();

    // Azure Container Instance Deletion
    // new Thread(() => azure.ContainerGroups.DeleteByResourceGroup(resourceGroup, agentName)).Start();
        
    return name != null
        ? (ActionResult)new OkObjectResult($"Hello, {name}")
        : new BadRequestObjectResult("Please pass a name on the query string or in the request body");
}

Ces quelques lignes de code devraient nous permettre de démarrer un agent de build sur Azure Container Instances.

Pour la suppression du conteneur, nous pouvons également créer une autre Azure Function, commenter les lignes de création du conteneur et décommenter les lignes de suppression :)

Vous pouvez visualiser le fichier directement sur Gist

Note : Le code de l’Azure Function est volontairement simpliste (On devrait plutôt passer les valeurs de configuration par un système tel que les variables d’environnement plutôt que de s’en servir directement, etc.)

Note 2 : Vous avez probablement remarqué que la création/suppression du conteneur est effectuée dans un thread séparé. C’est principalement dû au fait que les agentless jobs possèdent un timeout de 20 secondes. Dans notre cas, ce n’est pas vraiment un problème étant donné que la phase de compilation “classique” possède une file d’attente qui permet au job de s’exécuter dès que l’agent de build se retrouve bien enregistré à l’agent pool. Cependant, si vous souhaitez également vérifier la bonne exécution du démarrage du container, vous pouvez utiliser le mode de Callback de cette tâche.

Nous avons ensuite besoin d’ajouter un fichier function.proj pour permettre la restauration du package NuGet Microsoft.Azure.Management.Fluent.

02-add-function-proj-file-to-restore-nuget-package.png

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <TargetFramework>netstandard2.0</TargetFramework>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Microsoft.Azure.Management.Fluent" Version="1.24.1" />
    </ItemGroup>
</Project>

Le contenu de ce fichier se trouve également sur le Gist (Et vous pouvez trouver plus d’infos sur la restauration des packages NuGet dans une Azure Function sur la documentation officielle)

Il est alors possible de tester l’Azure Function directement depuis le portail Azure et ainsi vérifier que l’agent de Build s’enregistre avec succès sur l'Agent Pool de notre Azure DevOps.

Appel d’une Azure Function depuis un agentless job d’Azure Pipelines

Maintenant que nous avons un moyen simple de gérer notre conteneur, voyons comment l’intégrer à notre Azure Pipeline, plus précisément au démarrage et à la fin de celle-ci.

Commençons par créer une nouvelle Azure Pipeline, et pour garder les choses simples, utilisons le visual designer :

03-use-azure-pipelines-visual-designer.png

Après avoir choisi le template de notre application (ASP.NET Core par exemple), assurons nous que le pool d’agent “self-hosted” où va s’enregistrer l’agent de build est bien sélectionné.

04-select-azure-pipelines-agent-pool.png

Puis ajoutons 2 agentless jobs, au début de la pipeline pour créer et démarrer le conteneur, ainsi qu’à la fin pour le supprimer. Pour chacun, ajoutons-y une tâche Invoke Azure Function.

05-call-azure-function-from-azure-pipelines-agentless-task.png

Cette tâche a besoin des configurations suivantes :

  • L’Azure function URL et la Function key qui sont trouvables depuis le bouton Get function URL,
  • Le choix de la méthode GET ou POST (Get dans mon exemple), ainsi que la valeur Container Name passée en paramètre.

Vous pouvez ajouter la variable $(Build.BuildId) à son nom pour vous assurez que le nom soit unique pour chaque exécution du job,

  • Le mode ApiResponse en tant que Completion event(évènement de fin) de la tâche, pour que la pipeline se poursuive dès que l’API de l’Azure Function a répondu. (Voir le mode de Completion event Callback si vous souhaitez vérifier le bon fonctionnement de l’Azure Function également).

Et voilà ! Essayons d’exécuter notre pipeline maintenant :

06-build-agent-on-demand-successfully-completed.png

Et boom, nous avons réussi à générer une application dotnet core avec un agent de build éphémère, pour un temps de build de 2m 36s, soit moins d’1 centime l’exécution ! (Si l’on se réfère aux précédents calculs)

Plutôt cool n’est-ce pas ? :)

Ça sera tout pour cette série Azure Pipelines x Azure Container Instances pour le moment. N’hésitez pas à donner votre feedback en commentaire ou y poser vos questions !

Que le code soit avec vous !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus