API Management

Industrialiser ses déploiements API Management

Oct 18, 2022

Romain LAPREE-KAMINSKI

La plupart des projets stratégiques dans le cloud sont conduits selon les pratiques DevOps afin notamment de réduire le temps de déploiement des besoins métiers mais aussi de renforcer la fiabilité et la stabilité des applications. L’infrastructure n’échappe pas à cette règle et de plus en plus de moyens permettent de provisionner des services cloud grâce à du code, c’est ce qu’on appelle « l’Infrastructure As Code » ou IAC.

Dans cet article, je vais m’intéresser au déploiement du service Azure API Management. Je vais utiliser et comparer trois techniques de provisionnement : une extension Azure DevOps, Pulumi et Azure Bicep.

Toutes les démos ci-dessous se basent sur l’API WeatherForecast du template de base d’une API ASP.NET Core. Elle a été déployée sur un Azure App Service app-rlk-test. Nous allons à chaque fois créer une instance API Management, créer un produit et y affecter des groupes, importer une API sur ce produit et créer une règle sur une opération.

Déploiement avec l’extension API Management Suite

Une extension Azure DevOps est un programme qui réalise un traitement lié à la phase d’intégration ou à la phase de déploiement. Elle masque la complexité sous-jacente du traitement. L’extension API Management Suite, disponible sur la MarketPlace, a été créé par Stephane Eyskens, MVP Azure. Elle gère le déploiement des principales fonctionnalités de votre ressource Azure API Management :

  • Créer/Mettre à jour un Produit
  • Créer/Mettre à jour une API versionnée ou non (depuis un Swagger, un WSDL ou une Azure Function)
  • Définir une Règle (global ou au niveau d’une Opération)
  • Gérer les révisions d’une API

Prérequis important : votre ressource API Management doit déjà avoir été créée au préalable.

Une fois la ressource déployée, tout se passe dans Azure DevOps. J’utilise ici le Designer de Release qui me paraît être le moyen plus accessible pour les non-initiés. En pratique, il est préférable d’utiliser des fichiers YAML versionnés dans votre gestionnaire de code source.

Un service connection permet à Azure DevOps de publier sur ma souscription Azure.

Création du produit « Free »

Cette tâche permet de créer un produit avec ses diverses propriétés : nom, statut (publié ou non), accès libre ou via une clé de souscription, règles et groupes. Le groupe « administrators » est obligatoire. J’ajoute également les autres groupes built-in « developers » et « guests ».

création de produit

Création de l’API « Weather »

La tâche de création d’API importe une API externe grâce à un fichier format JSON ou YAML. Dans mon cas, il se trouve sur l’App Service « app-rlk-test.azurewebsites.net/swagger/v1/swagger.json« . Si le fichier n’est pas déployé, vous pouvez simplement le mettre à disposition dans l’artefact de build ou directement depuis un repo Git. En plus de ce lien, il faut fournir un nom à cette API et un chemin, « api/weather« , qui lie un appel à l’instance APIM et l’API. Ainsi, un appel sur « <url-apim>/api/weather/weatherforecast » est redirigé vers « app-rlk-test.azurewebsites.net/weatherforecast « . Enfin, il faut préciser si une souscription est requise pour l’utiliser et éventuellement une règle globale à toutes les opérations de cette API. Ma règle limite le nombre d’appels par souscription à 1000 toutes les 5mins.

création api 1
création api 2

Création d’une policy sur l’opération GetWeatherForecast

La tâche de création de règle sur une opération est assez simple. Il suffit de fournir la règle en question en ciblant une opération de l’API. Une opération représente un point de terminaison de l’API. Ma règle supprime le header « x-powered-by » en sortie de l’API.

création règle

Il n’en faut pas plus pour que le déploiement du Produit, de l’API et de la Policy sur Azure API Management ne soit industrialisé !

Cette technique a l’avantage d’être facilement appréhendable, il n’est pas nécessaire de maitriser un langage spécifique. En revanche, certaines opérations ne sont pas possibles (ex: création de l’instance APIM) ou sont limitées (définition de règle complexe, ajout d’approbation sur les produits). Sur les projets dans lesquels cette méthode était en place, j’ai été amené à contourner ces limites en créant des scripts PowerShell. Notez tout de même que vous pouvez contribuer au développement de l’extension sur le GitHub du projet.

Déploiement avec Pulumi

Pulumi est une plateforme d’Infrastructure As Code multi-langage (C#, F#, Java, Node.js, Python, YAML…) et multi-provider (Azure, AWS, Google Cloud, Kubernetes…). Ici, je vais utiliser le langage C#.

Création de l’instance Azure API Management

En utilisant Pulumi, je peux créer l’instance d’API Management à partir du code C#.

private const string RG_NAME = "rg-apim-deployment";
private const string APIM_INSTANCE_NAME = "apim-rlk-pulumi-test";
private const string APIM_LOCATION = "francecentral";

private ApiManagementService CreateOrUpdateApimService()
{
    return new ApiManagementService("apimService", new ApiManagementServiceArgs()
    {
        ResourceGroupName = RG_NAME,
        Location = APIM_LOCATION,
        ServiceName = APIM_INSTANCE_NAME,
        PublisherName = "Romain LAPREE-KAMINSKI",
        PublisherEmail = "romain.lapree@dcube.fr",
        EnableClientCertificate = true,
        Sku = new ApiManagementServiceSkuPropertiesArgs
        {
            Capacity = 1,
            Name = SkuType.Developer,
        }
    });
}

Attention, le création d’une instance d’API Management prend du temps dans Azure puisque cela dure environ 40 à 50 minutes selon Microsoft.

Création du produit « Free »

Je crée maintenant le nouveau produit « Free » sur lequel j’accorde les droits pour les groupes built-in « developers » et « guests », en plus du groupe par défaut « administrators ». Notez qu’avec l’extension Azure DevOps, il était nécessaire de préciser le groupe « administrators » alors qu’ici c’est implicite.

private static Product CreateOrUpdateFreeProduct(ApiManagementService apimService)
{
    var freeProduct = new Product("freeProduct", new ProductArgs()
        {
            ResourceGroupName = RG_NAME,
            ServiceName = APIM_INSTANCE_NAME,
            DisplayName = "Free",
            ProductId = "Free",
            State = ProductState.Published,
            SubscriptionRequired = false,
        },
        new CustomResourceOptions()
        {
            DependsOn = { apimService }
        }
    );

    var freeGroupGuests = new ProductGroup("freeGroupGuests", new ProductGroupArgs()
        {
            ResourceGroupName = RG_NAME,
            ServiceName = APIM_INSTANCE_NAME,
            ProductId = freeProduct.DisplayName,
            GroupId = "guests",
        },
        new CustomResourceOptions()
        {
            DependsOn = { freeProduct }
        }
    );

    var freeGroupDev = new ProductGroup("freeGroupDev", new ProductGroupArgs()
        {
            ResourceGroupName = RG_NAME,
            ServiceName = APIM_INSTANCE_NAME,
            ProductId = freeProduct.DisplayName,
            GroupId = "developers",
        },
        new CustomResourceOptions()
        {
            DependsOn = { freeProduct }
        }
    );
    return freeProduct;
}

Pour préciser l’ordre de déploiement des différents éléments, il faut préciser les dépendances entre ressources grâce à CustomResourceOptions et sa propriété DependsOn.

Création de l’API « Weather »

Ensuite j’importe l’API « Weather » sur laquelle j’enregistre le produit « Free ». Je mets également en place une règle pour limiter le nombre d’appels par adresse IP.

private static Api CreateOrUpdateWeatherAPI(Product freeProduct)
{
    var apiWeather = new Api("apiWeather", new ApiArgs()
    {
        ResourceGroupName = RG_NAME,
        ServiceName = APIM_INSTANCE_NAME,
        ApiId = "Weather",
        DisplayName = "Weather",
        Protocols = new InputList<Protocol> { Protocol.Https },
        Format = "openapi-link",
        Path = "api/wheather",
        Value = @"https://app-rlk-test.azurewebsites.net/swagger/v1/swagger.json",
    },
    new CustomResourceOptions()
    {
        DependsOn = { freeProduct }
    });

    var apiWeatherInFree = new ProductApi("apiWeatherInFree", new ProductApiArgs()
    {
        ResourceGroupName = RG_NAME,
        ServiceName = APIM_INSTANCE_NAME,
        ApiId = apiWeather.Name,
        ProductId = freeProduct.DisplayName
    },
    new CustomResourceOptions()
    {
        DependsOn = { freeProduct, apiWeather }
    });

    var apiWeatherPolicy = new ApiPolicy("apiWeatherPolicy", new ApiPolicyArgs()
    {
        ResourceGroupName = RG_NAME,
        ServiceName = APIM_INSTANCE_NAME,
        ApiId = apiWeather.Name,
        PolicyId = "policy",
        Format = "xml",
        Value =
        @"<policies>
	        <inbound>
                <quota-by-key calls=""1000"" renewal-period=""300"" counter-key=""@(context.Request.IpAddress)"" />
                <base />
	        </inbound>
	        <backend>
		        <base />
	        </backend>
	        <outbound>
		        <base />
	        </outbound>
	        <on-error>
		        <base />
	        </on-error>
        </policies>"
    },
    new CustomResourceOptions()
    {
        DependsOn = { apiWeather }
    });
    return apiWeather;
}

Création d’une policy sur l’opération GetWeatherForecast

Enfin, la dernière étape est de créer une règle sur l’opération « GetWeatherForecast » de l’API « Weather ». Je retire simplement le header « x-powered-by » de la réponse.

private static void CreateOrUpdateGetWeatherForecastPolicy(Api apiWeather)
{
    var getWeatherOperation = new ApiOperationPolicy("getWeatherOperation",
        new ApiOperationPolicyArgs()
        {
            ResourceGroupName = RG_NAME,
            ServiceName = APIM_INSTANCE_NAME,
            ApiId = apiWeather.Name,
            PolicyId = "policy",
            OperationId = "GetWeatherForecast",
            Format = "xml",
            Value = @"<policies>
                        <inbound>
                            <base />
                        </inbound>
                        <backend>
                            <base />
                        </backend>
                        <outbound>
                            <base />
                            <set-header name=""x-powered-by"" exists-action=""delete"" />
                        </outbound>
                        <on-error>
                            <base />
                        </on-error>
                    </policies>"
        },
        new CustomResourceOptions()
        {
            DependsOn = { apiWeather }
        }
    );
}

Déploiement via Azure DevOps

Pour la CI/CD, je passe par un fichier YAML constitué de deux étapes, le build et le deploy. Il s’agit donc dans un premier temps de compiler une application .NET 6 classique puis de publier le résultat de la compilation dans un artefact.

Pour le déploiement, il faut au préalable créer un compte sur le site Pulumi app.pulumi.com. Le mien se nomme « rlapreekaminski ». J’ai également besoin d’un token qui va permettre à Azure DevOps d’envoyer dans notre compte toutes les informations sur les déploiements. Je stocke ce jeton dans une groupe de variable Azure DevOps de manière sécurisée. En production, il faudra passer par un Azure Key Vault.

Avec Pulumi, tout ce qui concerne le déploiement, en particulier les ressources et l’historique, est rassemblé dans une Stack. Celle-ci sera créée lors du premier déploiement sous le nom « rlapreekaminski/pulumiApim/test ».

Trois étapes sont nécessaires dans le fichier YAML pour procéder au déploiement :

  1. Récupérer l’artefact généré lors de l’étape de build
  2. Installer le provider Azure-Native pour Pulumi. Ce provider interagit directement avec les APIs Microsoft, alors que le provider Azure Classique passe par Terraform.
  3. Lancer la commande « pulumi up » dont la syntaxe se trouve sur la documentation officielle
trigger:
  - main

name: Deploy Pulumi infra

variables:
- group: 'Secrets'
- name: 'azureServiceConnection'
  value : 'RomainLK Subscription'
- name: 'pulumiStack'
  value : 'rlapreekaminski/pulumiApim/test'
  
pool:
  vmImage: windows-latest

stages:
- stage: Build
  jobs:
  - job: Build
    steps:

      - task: DotNetCoreCLI@2
        displayName: "Restore nuget packages"
        inputs:
          command: "restore"
          projects: "**/*.csproj"
          feedsToUse: "select"

      - task: DotNetCoreCLI@2
        displayName: "Run dotnet build Release"
        inputs:
          command: "build"
          projects: "pulumiApim/*.csproj"
          arguments: "--configuration Release 
          --output $(Build.ArtifactStagingDirectory) --no-restore"
          
      - task: PublishBuildArtifacts@1
        displayName: "Publish .net artifact"
        inputs:
          pathtoPublish: $(Build.ArtifactStagingDirectory)
          artifactName: 'pulumi'

- stage: deploy
  jobs:
  - job: Deploy
    timeoutInMinutes: 0
    steps:

        - script: 'pulumi plugin install resource azure-native v1.66.0'
          displayName: 'Install Pulumi azure-native resource'

        - task: DownloadBuildArtifacts@0
          inputs:
            artifactName: pulumi
            downloadPath: $(System.ArtifactsDirectory)

        - task: pulumi.build-and-release-task.custom-build-release-task.Pulumi@1
          displayName: 'Run pulumi up'
          inputs:
            azureSubscription: $(azureServiceConnection)
            command: up
            args: '--yes'
            cwd: '$(System.ArtifactsDirectory)/pulumi'
            stack: $(pulumiStack)
            createStack: true
            

La tâche « Run Pulumi up » exécute le déploiement de l’infrastructure dans Azure grâce au code C# compilé. Elle utilise la commande « up » de l’exécutable pulumi.exe. Un paramètre createStack spécifie que la Stack « rlapreekaminski/pulumiApim/test » doit être créée si elle n’existe pas.

Si votre projet Azure DevOps est privé et si l’agent de déploiement utilisé est celui offert par Microsoft, le timeout du pipeline est de 60 minutes. Même si Microsoft annonce une création d’instance API Management en 50min, dans la pratique cela dure près de 1h20. Ainsi dans la majorité des cas, votre premier déploiement qui crée l’instance échoue. Même si la ressource est bien présente et active dans votre groupe de ressources, la Stack Pulumi sera désynchronisée, rendant tout déploiement futur impossible. Il faut donc soit ajuster l’agent de déploiement, soit créer la ressource Azure Api Management à la main.

Déploiement avec Azure Bicep

Azure Bicep est un langage créé par Microsoft, utilisable en production depuis sa version v0.3 (mai 2021), qui permet de créer des ressources Azure grâce à une syntaxe assez proche du langage utilisé par Terraform de part sa structure et ses capacités modulaires. Il se veut plus simple et plus digeste qu’un template ARM.

L’avantage principal, selon moi, d’Azure Bicep sur des framework d’IAC tels que Terraform ou Pulumi est qu’il prend en charge les services Azure en préversion et toutes les nouveautés des services existants dès leur sortie, sans attendre la nouvelle version d’un provider. Autre différence majeure, Azure Bicep ne se base pas sur un fichier historisant les ressources créées comme le State Terraform ou la Stack Pulumi. Vous pouvez trouver plus d’information sur la documention officielle.

Création de l’infrastructure

Les différentes étapes (création de l’instance, du produit, de l’API et des règles) pour monter l’infrastructure cible avec Azure Bicep sont très similaires à celles vues précédemment avec Pulumi.

@description('Location for all resources.')
param location string = resourceGroup().location

resource apimService 'Microsoft.ApiManagement/service@2021-08-01' = {
  name: 'apim-rlk-bicep-test'
  location: location
  sku: {
    name: 'Developer'
    capacity: 1
  }
  properties: {
    publisherEmail: 'romain.lapree@dcube.fr'
    publisherName: 'Romain LAPREE-KAMINSKI'
  }
}

resource freeProduct 'Microsoft.ApiManagement/service/products@2021-12-01-preview' = {
  name: 'Free'
  parent: apimService
  properties: {
    displayName: 'Free'
    state: 'published'
    subscriptionRequired: false
  }
}

resource freeGroupGuests 'Microsoft.ApiManagement/service/products/groups@2021-12-01-preview' = {
  name: 'guests'
  parent: freeProduct
}

resource freeGroupDevelopers 'Microsoft.ApiManagement/service/products/groups@2021-12-01-preview' = {
  name: 'developers'
  parent: freeProduct
}

resource apiWeather 'Microsoft.ApiManagement/service/apis@2021-12-01-preview' = {
  name: 'Weather'
  parent: apimService
  properties: {
    displayName: 'Weather'
    format: 'openapi-link'
    path: 'api/wheather'
    protocols: [
      'https'
    ]
    serviceUrl: 'https://app-rlk-test.azurewebsites.net'
    value: 'https://app-rlk-test.azurewebsites.net/swagger/v1/swagger.json'
  }
}

resource apiWeatherInFree 'Microsoft.ApiManagement/service/products/apis@2021-12-01-preview' = {
  name: 'Weather'
  parent: freeProduct
  dependsOn: [
    apiWeather
  ]
}

resource apiWeatherPolicy 'Microsoft.ApiManagement/service/apis/policies@2021-12-01-preview' = {
  name: 'policy'
  parent: apiWeather
  properties: {
    format: 'xml'
    value: '<policies>rn <inbound> rn <quota-by-key calls="1000" renewal-period="300" counter-key="@(context.Request.IpAddress)" /> rn <base /> rn </inbound> rn <backend> rn <base /> rn </backend><outbound>rn <base />rn </outbound>rn <on-error>rn <base />rn  </on-error>rn</policies>'
  }
}

resource getOperation 'Microsoft.ApiManagement/service/apis/operations@2021-12-01-preview' existing = {
  name: 'GetWeatherForecast'
  parent: apiWeather
  resource getPolicy 'policies@2021-12-01-preview' = {
    name: 'policy'
    properties: {
      format: 'xml'
      value: '<policies>rn <inbound> rn <base /> rn </inbound> rn <backend> rn <base /> rn </backend><outbound>rn <base /> <set-header name="x-powered-by" exists-action="delete" />rn </outbound>rn <on-error>rn <base />rn  </on-error>rn</policies>'
    }
  }
}

Pour gérer les relations entre les différents éléments, plusieurs possibilités :

  • les imbriquer : l’enfant est déclaré à l’intérieur du parent
  • les séparer : soit en utilisant la propriété parent à l’intérieur de l’enfant, soit en définissant l’arborescence relative dans le nom de la ressource

C’est un peu particulier pour la règle sur l’opération puisque l’opération en question n’est pas directement créée dans notre fichier Bicep. Elle l’est mais à travers l’import de l’API depuis le fichier JSON. Pour contourner cela, on utilise le mot existing qui permet de référencer un parent qui existe déjà.

Si en revanche deux ressources ne sont pas directement liées entre elles, mais que la création de l’une doit se faire après la création de l’autre, alors il faut préciser cette dépendance explicitement avec le mot dependsOn. C’est le cas pour l’affectation du produit Free à notre API Weather. Cette association a pour parent le produit mais doit être faite après la création de l’API.

Déploiement

Pour le déploiement, tout comme pour Pulumi, je passe par un fichier YAML constitué de deux étapes, le build et le deploy. Dans le job de build, le fichier Bicep va être compilé en fichier ARM grâce à la commande az bicep build puis publié dans un artefact. Il n’est pas utilisé pour le déploiement mais permet de garder un historique. Dans le job de deploy, j’utilise une tâche d’Azure CLI et la commande az deployment group

trigger:
  branches:
    include:
    - main
  paths:
    include:
      - 'bicepApim'

name: Deploy Bicep files

variables:
  vmImageName: 'ubuntu-latest'

  azureServiceConnection: 'RomainLK Subscription (a25de04e-a160-401e-9168-7abafdf652bd)'
  resourceGroupName: 'rg-apim-deployment'
  location: 'francecentral'
  
pool:
  vmImage: $(vmImageName)

stages:
- stage: Build
  jobs:
  - job: Build
    steps:
        - task: AzureCLI@2  
          displayName: 'build bicep artifact' 
          inputs: 
            azureSubscription: $(azureServiceConnection) 
            scriptType: 'pscore'  
            scriptLocation: 'inlineScript'  
            inlineScript: 'az bicep build --file ./bicepApim/main.bicep'  
            
        - task: PublishBuildArtifacts@1 
          displayName: 'Publish artifact in pipeline' 
          inputs: 
            PathtoPublish: '$(Build.SourcesDirectory)/bicepApim/main.json'  
            ArtifactName: 'finishedTemplate'  
            publishLocation: 'Container' 
            
- stage: deploy
  jobs:
  - job: Deploy
    steps:
        - task: AzureCLI@2
          displayName: 'deploy bicep template'
          inputs:
            azureSubscription: $(azureServiceConnection) 
            scriptType: 'pscore'
            scriptLocation: 'inlineScript'
            inlineScript: |
              az deployment group create  `
              --template-file $(Build.SourcesDirectory)/bicepApim/main.bicep `
              --parameters $(Build.SourcesDirectory)/bicepApim/main.parameters.json `
              --resource-group $(resourceGroupName)

Une chose importante différencie le déploiement Bicep du déploiement Pulumi : si votre job atteint un timeout et tombe en échec, vous pourrez le relancer et il fonctionnera parfaitement grâce à l’absence de fichier d’état.

Conclusion

Je vous ai présenté ici trois manières d’industrialiser le déploiement d’une ressource Azure API Management, chacune présentant des avantages et des inconvénients :

  • Une extension Azure DevOps API Management Suite qui ne nécessite pas la connaissance d’un langage de programmation mais n’est pas suffisant pour une configuration complexe
  • Pulumi qui peut être utilisé si vos équipes maitrisent déjà un langage de programmation mais qui ne permet pas d’avoir les dernières nouveautés du service dès leur sortie en preview
  • Azure Bicep qui nécessite l’apprentissage, bien que rapide, d’un langage spécifique que vous ne pourrez pas utiliser sur d’autres providers. Il permet en revanche d’utiliser les nouveautés dès leur sortie, et ne se base pas sur un fichier d’historique.

J’espère que cet article vous aidera d’une part à ne plus gérer votre service Azure API Management à la main mais également à choisir l’une des méthodes présentées.

0 commentaires

Soumettre un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Découvrez nos autres articles

git et la face cachée du Rebase

git et la face cachée du Rebase

"Faire une rebase ? *sight* heu... ok..." Jean-Michel Fullstack - Développeur fébrile Jean-Michel est inquiet. En effet, lorsque nous collaborons à plusieurs sur un projet, quelque soit les technologies utilisées, il est important de garder à l'esprit que notre...

lire plus
Aller au contenu principal