Abonnez-vous pour recevoir des notifications sur les nouveaux articles :

Génération automatique du fournisseur Terraform de Cloudflare

2024-09-24

Lecture: 10 min.
Cet article est également disponible en English, en 繁體中文, en Deutsch, en 日本語, en 한국어, en Español et en 简体中文.

En novembre 2022, nous avons annoncé la transition de l'API Cloudflare vers les schémas OpenAPI. À l'époque, nous avions un objectif audacieux : faire des schémas OpenAPI la source unique d'informations fiables pour notre écosystème de SDK et notre documentation de référence. Au cours de la Developer Week 2024, nous avons confirmé cet objectif, avec l'annonce de la génération automatique de nos bibliothèques de SDK à partir de ces schémas OpenAPI. Aujourd'hui, nous sommes heureux d'annoncer les nouveaux composants de l'écosystème qui bénéficient désormais de la génération automatique : le fournisseur Terraform et la documentation de référence de l'API.

Désormais, dès qu'une fonctionnalité ou un attribut nouveaux seront ajoutés à nos produits et documentés par l'équipe, vous pourrez voir de quelle manière ils sont censés être utilisés au sein de notre écosystème de SDK et les utiliser immédiatement. Plus de retards, et plus de lacunes dans la couverture des points de terminaison d'API. 

Vous trouverez le nouveau site de documentation à l'adresse https://developers.cloudflare.com/api-next/, et vous pouvez tester la version d'évaluation préliminaire du fournisseur Terraform en installant 5.0.0-alpha1.

Pourquoi Terraform ? 

Pour tous ceux qui ne connaissent pas Terraform, il s'agit d'un outil assurant la gestion de votre infrastructure en tant que code, de la même manière que vous gérez le code de votre application. Nombre de nos clients (grands et petits) s'appuient sur Terraform pour bénéficier de l'orchestration indépendante de la technologie de leur infrastructure. Dans l'envers du décor, Terraforme est fondamentalement un client HTTP avec une gestion intégrée du cycle de vie ; cela signifie que le logiciel utilise nos API documentées publiquement selon une approche comprenant la création, la lecture, la mise à jour et la suppression sur l'ensemble de la durée de vie de la ressource. 

Gérer la mise à jour de Terraform – à l'ancienne

Historiquement, Cloudflare assurait manuellement la maintenance d'un fournisseur Terraform. Toutefois, puisque les rouages internes du fournisseur nécessitent une façon de faire bien particulière, la responsabilité de la maintenance et du support incombait à une poignée de personnes seulement. Les équipes de maintenance ont toujours eu des difficultés à suivre le rythme des changements, en raison de l'importante charge cognitive que nécessite la livraison de la moindre modification du fournisseur. Tout modification du fournisseur par une équipe nécessitait au moins trois requêtes d'extraction (voire quatre, si l'on ajoutait le support de cf-terraforming).

Même lorsque les quatre requêtes d'extraction étaient finalisées, cela n'offrait aucune garantie concernant la couverture de l'ensemble des attributs disponibles. Des détails infimes, mais importants étaient donc susceptibles d'être omis et, par conséquent, de ne pas être exposés aux clients, ce qui s'avérait frustrant lors d'une tentative de configuration d'une ressource.

Pour y remédier, notre fournisseur Terraform devait s'appuyer sur les mêmes schémas OpenAPI que ceux dont bénéficiait déjà le reste de notre écosystème de SDK.

Mettre Terraform à jour automatiquement

Terraform se distingue de nos SDK par sa capacité à gérer le cycle de vie des ressources. Cette capacité entraîne une nouvelle série de problèmes liés aux valeurs connues et à la gestion des différences entre les charges utiles des requêtes et des réponses. Comparons les deux approches différentes de la création d'un nouvel enregistrement DNS et de sa récupération.

Avec notre SDK Go :

// Create the new record
record, _ := client.DNS.Records.New(context.TODO(), dns.RecordNewParams{
	ZoneID: cloudflare.F("023e105f4ecef8ad9ca31a8372d0c353"),
	Record: dns.RecordParam{
		Name:    cloudflare.String("@"),
		Type:    cloudflare.String("CNAME"),
        Content: cloudflare.String("example.com"),
	},
})


// Wasteful fetch, but shows the point
client.DNS.Records.Get(
	context.Background(),
	record.ID,
	dns.RecordGetParams{
		ZoneID: cloudflare.String("023e105f4ecef8ad9ca31a8372d0c353"),
	},
)

Et avec Terraform :

resource "cloudflare_dns_record" "example" {
  zone_id = "023e105f4ecef8ad9ca31a8372d0c353"
  name    = "@"
  content = "example.com"
  type    = "CNAME"
}

De prime abord, l'approche de Terraform paraît plus simple (et vous auriez raison de croire que c'est le cas). La complexité des connaissances qu'exigent la création d'une nouvelle ressource et la gestion des modifications est gérée à votre place. Cependant, le problème est le suivant : pour que Terraform puisse proposer cette abstraction et cette garantie de données, toutes les valeurs doivent être connues au moment de l'application. Cela signifie que même si vous n'utilisez pas la valeur traitée en proxy, Terraform doit connaître la valeur requise afin de l'enregistrer dans le fichier d'état et de gérer cet attribut à l'avenir. L'erreur ci-dessous est celle qu'observent régulièrement les opérateurs Terraform ; elle est renvoyée par les fournisseurs lorsque la valeur n'est pas connue au moment de l'application.

Error: Provider produced inconsistent result after apply

When applying changes to example_thing.foo, provider "provider[\"registry.terraform.io/example/example\"]"
produced an unexpected new value: .foo: was null, but now cty.StringVal("").

Lorsque vous utilisez les SDK, en revanche, si vous n'avez pas besoin d'un champ, il vous suffit de l'omettre ; vous n'aurez jamais à vous préoccuper de gérer des valeurs connues.

Remédier à ce problème pour nos schémas OpenAPI n'a pas été une mince affaire. Depuis l'introduction de la prise en charge de la génération de Terraform, la qualité de nos schémas s'est considérablement améliorée. Maintenant, nous désignons explicitement toutes les valeurs par défaut présentes, les propriétés de réponse variables basées sur la charge utile de la requête ainsi que les éventuels attributs calculés côté serveur. Tout ceci permet de proposer une meilleure expérience à toutes les personnes qui interagissent avec nos API.

Passer de terraform-plugin-sdk à terraform-plugin-framework

Pour développer un fournisseur Terraform et exposer les ressources ou les sources de données aux opérateurs, vous avez besoin de deux choses essentielles : un serveur fournisseur et un fournisseur.

Le serveur fournisseur se charge d'exposer un serveur gRPC utilisé par le cœur Terraform (via l'interface de ligne de commande) pour communiquer lors de la gestion des ressources ou de la lecture des sources de données issues de la configuration fournie par l'opérateur.

Le fournisseur est responsable de l'enveloppement des ressources et des sources de données, de la communication avec les services à distance et de la gestion du fichier d'état. À cette fin, vous pouvez soit vous fier au plugin terraform-plugin-sdk (communément appelé SDKv2) ou au framework terraform-plugin-framework, qui inclut l'ensemble des interfaces et des méthodes fournies par Terraform afin de gérer correctement les rouages internes de la solution. La décision concernant le choix du plugin utilisé dépend de l'âge de votre fournisseur. SDKv2 existe depuis plus longtemps et est plébiscité par la plupart des fournisseurs de Terraform. En raison de son ancienneté et de sa complexité, toutefois, il comporte de nombreux problèmes fondamentaux non résolus, qui doivent rester en l'état afin de faciliter la rétrocompatibilité pour les développeurs qui l'utilisent. Le framework terraform-plugin-framework est la nouvelle version ; et bien qu'elle n'offre pas l'étendue des fonctionnalités de SDKv2, elle propose une approche plus semblable à Go de la création de fournisseurs et corrige un grand nombre des bugs sous-jacents présents dans SDKv2.

(Pour une comparaison plus approfondie entre SDKv2 et le framework, vous pouvez consulter un échange entre moi-même et John Bristowe d'Octopus Deploy.)

Le fournisseur Cloudflare Terraform a été majoritairement développé avec SDKv2. Au début de 2023, toutefois, nous avons relevé le défi du multiplex, et nous proposons désormais les deux plugins dans notre fournisseur. Pour comprendre pourquoi cette démarche était nécessaire, nous devons vous donner quelques explications concernant SDKv2. La structure de SDKv2 n'est pas vraiment propice à la représentation cohérente et fiable de valeurs nulles ou « non définies ». Vous pouvez utiliser la fonction expérimentale ResourceData.GetRawConfig pour vérifier si une valeur est définie (set), nulle (null) ou inconnue (unknown) dans la configuration, mais sa réécriture sous forme de valeur nulle n'est pas vraiment prise en charge.

Nous avons pris conscience pour la première fois de cette problématique lorsque Edge Rules Engine (Rulesets) a commencé l'intégration de nouveaux services qui devaient prendre en charge des réponses d'API contenant des valeurs booléennes dans un état non défini (ou manquant), true ou false, chacune avec un raisonnement et une finalité propres. Bien qu'il ne s'agisse pas d'un modèle d'API conventionnel chez Cloudflare, il s'agit d'une approche valide, et nous devrions pouvoir nous en accommoder. Toutefois, comme nous l'avons indiqué ci-dessus, ce n'était pas le cas du fournisseur SDKv2. En effet, lorsqu'une valeur n'est pas présente dans la réponse ou lue dans l'état, elle reçoit une valeur zéro compatible avec Go pour l'état par défaut. Cette situation s'est traduite par l'impossibilité d'annuler l'état de valeurs après qu'elles aient été écrites dans l'état sous forme de false (et inversement).

La seule solution que nous avons ici pour garantir l'utilisation fiable des trois états de ces valeurs booléennes consiste à migrer vers le framework terraform-plugin-framework, qui propose la mise en œuvre correcte de la fonction de réécriture des valeurs non définies.

Lorsque nous avons commencé à ajouter des fonctionnalités supplémentaires dans l'ancien fournisseur avec terraform-plugin-framework, il est devenu manifeste que l'expérience des développeurs était meilleure. Nous avons donc ajouté un ratchet afin d'empêcher l'utilisation de SDKv2 à l'avenir ; ainsi, nous pourrions devancer les utilisateurs qui, à leur insu, se préparaient à se heurter à ce problème.

Lorsque nous avons pris la décision de générer automatiquement le fournisseur Terraform, il nous a paru logique d'intégrer également toutes les ressources ; l'ensemble reposerait ainsi sur terraform-plugin-framework et nous pourrions, une fois pour toutes, reléguer au passé les problèmes liés à SDKv2. Cette démarche a compliqué le déroulement de la migration, car l'amélioration des mécanismes internes a nécessité la modification de composants majeurs tels que le schéma et les opérations CRUD, avec lesquels nous devions nous familiariser. Cependant, l'investissement en valait la peine, car cette migration nous a permis de pérenniser les fondations du fournisseur, et nous sommes désormais moins souvent contraints d'accepter des compromis affectant l'expérience dans Terraform en raison de mécanismes internes hérités et défaillants.

Recherche itérative de bugs 

L'un des défis les plus courants concernant les pipelines de génération de code est qu'à moins de disposer d'outils existants pour mettre en œuvre votre nouvelle fonctionnalité, il est difficile de savoir si elle est opérationnelle ou s'il est raisonnable de l'utiliser. Bien entendu, vous pouvez également générer vos tests afin d'évaluer la nouvelle fonctionnalité dans la pratique ; toutefois, si un bug est présent dans le pipeline, il est très probable que vous ne le perceviez pas comme un bug, car vous générerez des assertions de test démontrant que le bug est un comportement attendu. 

L'une des boucles de collecte d'informations essentielles dont nous disposons est la suite existante de tests d'acceptation. Toutes les ressources incluses au fournisseur existant comportaient un mélange de tests de régression et de tests de fonctionnalité. Cerise sur le gâteau, dans la mesure où la suite de tests crée et gère des ressources réelles, il était très facile de déterminer si le résultat était une implémentation fonctionnelle ou non : il suffisait d'examiner le trafic HTTP afin de déterminer si les appels d'API étaient acceptés par les points de terminaison distants. Pour finaliser la transposition de la suite de tests, il nous a suffi de copier l'ensemble des tests existants et de rechercher d'éventuelles différences d'assertion de type (par exemple, une liste transposée en liste imbriquée unique) avant de lancer une exécution test afin de déterminer si la ressource fonctionnait correctement. 

Si le pipeline de schémas centralisé a considérablement amélioré la qualité de vie des développeurs en permettant la propagation presque instantanée des correctifs de schémas à l'ensemble de l'écosystème, il n'a pas pu nous aider à résoudre le problème le plus important, à savoir l'identification de bugs dissimulant d'autres bugs. Ce processus demandait du temps, car lorsque vous résolvez un problème dans Terraform, il existe trois emplacements dans lesquels vous pouvez rencontrer une erreur :

  1. Avant d'effectuer un appel d'API, Terraform met en œuvre une validation de schéma logique et, si le programme rencontre des erreurs de validation, il s'arrête immédiatement.

  2. Si un appel d'API échoue, le programme s'arrête à l'opération CRUD et renvoie les diagnostics, en s'arrêtant immédiatement.

  3. Après l'exécution de l'opération CRUD, Terraform met en œuvre des contrôles afin d'assurer que toutes les valeurs sont connues. 

Par conséquent, si nous nous heurtions à un bug à l'étape 1 et nous corrigions ce bug, rien ne garantissait que nous n'allions pas découvrir deux autres bugs – et nous n'avions aucun moyen de le savoir. Par ailleurs, si nous découvrions un bug à l'étape 2 et publiions un correctif, cela ne nous permettrait pas pour autant d'identifier, lors de la série de tests suivante, un bug à l'étape 1.

Il n'existe ici aucune solution miracle ; notre approche consistait donc plutôt à identifier des modèles de problèmes dans les comportements des schémas et à appliquer les règles de l'outil CI lint au sein des schémas OpenAPI avant qu'ils n'atteignent le pipeline de génération de code. L'adoption de cette approche a permis de progressivement réduire le nombre de bugs aux étapes 1 et 2, jusqu'à ce que les problèmes se limitent, en grande partie, à la gestion du type à l'étape 3.

Une approche plus réutilisable de la conversion de modèles et de structures 

Dans les opérations CRUD du fournisseur Terraform, il est assez courant de voir du code standard semblable à l'exemple suivant :

var plan ThingModel
diags := req.Plan.Get(ctx, &plan)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
	return
}

out, err := r.client.UpdateThingModel(ctx, client.ThingModelRequest{
	AttrA: plan.AttrA.ValueString(),
	AttrB: plan.AttrB.ValueString(),
	AttrC: plan.AttrC.ValueString(),
})
if err != nil {
	resp.Diagnostics.AddError(
		"Error updating project Thing",
		"Could not update Thing, unexpected error: "+err.Error(),
	)
	return
}

result := convertResponseToThingModel(out)
tflog.Info(ctx, "created thing", map[string]interface{}{
	"attr_a": result.AttrA.ValueString(),
	"attr_b": result.AttrB.ValueString(),
	"attr_c": result.AttrC.ValueString(),
})

diags = resp.State.Set(ctx, result)
resp.Diagnostics.Append(diags...)
if resp.Diagnostics.HasError() {
	return
}

À un niveau élevé :

  • Nous récupérons les mises à jour proposées (appelées « plan ») avec req.Plan.Get()

  • Exécuter l'appel d'API de mise à jour avec les nouvelles valeurs

  • Manipuler les données depuis un type Go dans un modèle Terraform (convertResponseToThingModel)

  • Définir l'état en appelant resp.State.Set()

Au départ, cela ne semble pas trop problématique. Toutefois, l'étape 3, lors de laquelle nous manipulons le type Go afin de l'intégrer au modèle Terraform, devient rapidement fastidieuse, sujette à erreurs et complexe, car toutes les ressources doivent suivre cette procédure pour effectuer la permutation entre le type et les modèles Terraform associés.

Pour éviter de générer du code d'une complexité superflue, nous avons apporté l'amélioration suivante à notre fournisseur : toutes les méthodes CRUD utilisent des méthodes unifiées apijson.Marshal, apijson.Unmarshal, et apijson.UnmarshalComputed permettant de résoudre ce problème en centralisant la logique de conversion et de traitement en fonction des balises struct.

var data *ThingModel

resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
	return
}

dataBytes, err := apijson.Marshal(data)
if err != nil {
	resp.Diagnostics.AddError("failed to serialize http request", err.Error())
	return
}
res := new(http.Response)
env := ThingResultEnvelope{*data}
_, err = r.client.Thing.Update(
	// ...
)
if err != nil {
	resp.Diagnostics.AddError("failed to make http request", err.Error())
	return
}

bytes, _ := io.ReadAll(res.Body)
err = apijson.UnmarshalComputed(bytes, &env)
if err != nil {
	resp.Diagnostics.AddError("failed to deserialize http request", err.Error())
	return
}
data = &env.Result

resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)

Au lieu de devoir générer des centaines d'instances de méthodes de convertisseur de type en modèle, nous pouvons, à la place, décorer le modèle Terraform avec les balises correctes et gérer le marshaling des données de manière cohérente. Il s'agit d'une modification mineure du code qui, à long terme, rend la génération plus réutilisable et plus lisible. Cette approche offre également l'avantage de faciliter considérablement la correction des bugs. En effet, lorsque vous avez identifié un bug avec un type de champ particulier, la correction de ce bug dans l'interface unifiée entraîne également sa correction pour d'autres événements que vous n'avez peut-être pas encore identifiés.

Mais attendez, ce n'est pas fini... documentation à l'appui !

Pour compléter notre utilisation des schémas OpenAPI, nous renforçons l'intégration du SDK avec notre nouveau site de documentation de l'API. Ce site utilise le même pipeline dans lequel nous avons investi ces deux dernières années, tout en résolvant certaines problématiques courantes liées à l'utilisation.

Sensibilité au SDK 

Si vous avez déjà consulté notre site de documentation de l'API, vous savez que nous y proposons des exemples d'interactions avec l'API via des outils de ligne de commande tels que cURL. C'est un excellent point de départ ; toutefois, si vous utilisez l'une des bibliothèques du SDK, une certaine gymnastique mentale est nécessaire pour convertir la bibliothèque vers la méthode ou la définition de type que vous souhaitez utiliser. Maintenant que nous utilisons le même pipeline pour générer les SDK et la documentation, nous résolvons ce problème en fournissant des exemples dans toutes les bibliothèques que vous pouvez utiliser – pas uniquement curl.

Exemple utilisant cURL pour récupérer toutes les zones.

Exemple utilisant la bibliothèque Typescript pour récupérer toutes les zones.

Exemple utilisant la bibliothèque Python pour récupérer toutes les zones.

Exemple utilisant la bibliothèque Go pour récupérer toutes les zones.

Avec cette amélioration, nous mémorisons également la sélection du langage ; aussi, si vous avez choisi d'afficher la documentation avec notre bibliothèque Typescript, puis vous continuez à cliquer, nous continuerons à vous présenter des exemples utilisant Typescript jusqu'à ce que vous changiez de bibliothèque.

Mieux encore, lorsque nous introduisons de nouveaux attributs sur des points de terminaison existants ou nous ajoutons des langages au SDK, ce site de documentation est automatiquement synchronisé avec le pipeline. La mise à jour de l'ensemble de la documentation ne demande désormais plus un effort démesuré.

Un rendu plus rapide et plus efficace

Un problème auquel nous avons toujours été confrontés est le nombre considérable de points de terminaison d'API et la manière de les représenter. À ce jour, nous avons 1 330 points de terminaison – et, pour chacun d'entre eux, nous avons une charge utile de requête, une charge utile de réponse et une multitude de types associés. Pour restituer un tel volume d'informations, les solutions que nous avons utilisées par le passé nous ont imposé d'accepter des compromis pour assurer la fonctionnalité de certaines parties de la représentation.

Cette nouvelle itération du site de documentation de l'API résout ce problème de différentes façons :

  • Elle est mise en œuvre sous la forme d'une application React moderne, associant une expérience interactive côté client à des contenus statiques pré-rendus, ce qui permet d'obtenir un chargement initial et une navigation rapides. (Oui, cela fonctionne, même sans JavaScript !)

  • Le site récupère les données sous-jacentes progressivement, au fur et à mesure que vous parcourez la documentation.

En résolvant ce problème fondamental, nous avons ouvert la possibilité d'apporter d'autres améliorations prévues au site de documentation et à l'écosystème du SDK ; ceci nous permettra d'améliorer l'expérience utilisateur, toutefois, sans accepter de compromis, comme nous avons dû le faire par le passé. 

Autorisations

Les autorisations minimales requises pour les points de terminaison de l'API représentent l'une des réintégrations de fonctionnalités les plus fréquemment demandées pour le site de documentation. L'une des précédentes itérations du site de documentation proposait cette fonctionnalité. Cependant, à l'insu de la plupart des personnes qui l'utilisaient, les valeurs étaient gérées manuellement et se révélaient régulièrement incorrectes, ce qui entraînait l'ouverture de tickets d'assistance et générait de la frustration pour les utilisateurs. 

Dans le système de gestion des identités et des accès de Cloudflare, répondre à la question « de quoi ai-je besoin pour accéder à ce point de point de terminaison ? » n'est pas une tâche aisée. En effet, dans le flux normal d'une requête adressée au plan de contrôle, deux systèmes différents sont nécessaires pour fournir des parties de la question pouvant ensuite être réunies pour apporter une réponse complète. Comme nous ne pouvions pas initialement automatiser cet aspect dans le cadre du pipeline OpenAPI, nous avons choisi de le laisser de côté, au lieu d'accepter que son fonctionnement soit incorrect, sans aucun moyen de le vérifier. 

Aujourd'hui, nous sommes ravis d'annoncer le retour des autorisations pour les points de terminaison ! Nous avons développé de nouveaux outils qui résument la réponse, et nous pouvons intégrer cette approche dans notre pipeline de génération de code et transmettre automatiquement ces informations à tous les points de terminaison. À l'instar du reste de la plateforme de génération de code, ces outils ont pour finalité de permettre aux équipes de maintenance de détenir et gérer des schémas de qualité, qu'elles peuvent réutiliser en offrant un ajout de valeur, sans nécessiter davantage de travail de leur part.

N'attendez plus les mises à jour

Avec ces annonces, nous mettons un terme à l'attente de mises à jour au sein de l'écosystème du SDK. Ces nouvelles améliorations nous permettent de rationaliser la capacité des nouveaux attributs et des points de terminaison dès l'instant où les équipes les documentent. Alors, qu'attendez-vous ? Découvrez dès aujourd'hui le fournisseur Terraform et le site de documentation de l'API.

Nous protégeons des réseaux d'entreprise entiers, aidons nos clients à développer efficacement des applications à l'échelle d'Internet, accélérons tous les sites web ou applications Internet, repoussons les attaques DDoS, tenons les pirates informatiques à distance et pouvons vous accompagner dans votre parcours d'adoption de l'architecture Zero Trust.

Accédez à 1.1.1.1 depuis n'importe quel appareil pour commencer à utiliser notre application gratuite, qui rend votre navigation Internet plus rapide et plus sûre.

Pour en apprendre davantage sur notre mission, à savoir contribuer à bâtir un Internet meilleur, cliquez ici. Si vous cherchez de nouvelles perspectives professionnelles, consultez nos postes vacants.
Birthday Week (FR)API (FR)SDKTerraformOpen APIDeveloper PlatformAgile Developer ServicesDéveloppeurs

Suivre sur X

Jacob Bednarz|@jacobbednarz
Cloudflare|@cloudflare

Publications associées

31 octobre 2024 à 13:00

Moving Baselime from AWS to Cloudflare: simpler architecture, improved performance, over 80% lower cloud costs

Post-acquisition, we migrated Baselime from AWS to the Cloudflare Developer Platform and in the process, we improved query times, simplified data ingestion, and now handle far more events, all while cutting costs. Here’s how we built a modern, high-performing observability platform on Cloudflare’s network....