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
- Déploiement avec Pulumi
- Déploiement avec Azure Bicep
- Conclusion
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 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 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.
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 :
- Récupérer l’artefact généré lors de l’étape de build
- Installer le provider Azure-Native pour Pulumi. Ce provider interagit directement avec les APIs Microsoft, alors que le provider Azure Classique passe par Terraform.
- 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.
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