
Dans cet article, Cyon John, architecte Cloud, plonge dans le fonctionnement interne de Terraform en fournissant des conseils sur le développement de nouvelles ressources pour un fournisseur Terraform.
Avant de commencer, cet article est écrit en supposant que le lecteur est déjà familier avec Terraform et comment il fonctionne du point de vue de l’utilisateur final. Dans cet article, nous examinons une partie du fonctionnement interne de Terraform, en mettant l’accent sur le développement de nouvelles ressources pour un fournisseur Terraform. Cela ne couvre pas l’ajout d’un nouveau fournisseur lui-même, mais seulement l’ajout de ressources à un fournisseur existant.
Vue de haut niveau
Terraform dispose d’un modèle simple et hautement découplé qui permet à des tiers de développer et d’étendre facilement les fonctionnalités.
Terraform est divisé en un noyau et un certain nombre de plugins avec lesquels il interagit. Le noyau et chacun des plugins sont des exécutables binaires indépendants, liés statiquement. Le noyau charge les plugins et communique avec eux via RPC.
Le noyau fournit des fonctionnalités communes telles que :
- Prise en charge de la langue – CLI, HCL, interpolation, fonctions, modules, etc.
- Gestion de l’état
- Graphique de dépendance et exécution du plan
- Chargement de plugins et délégation du travail à des plugins
Terraform dépend de plugins pour provisionner/gérer les ressources et effectuer toutes les actions sur celles-ci. Il existe deux types de plugins :
- Plugins de fournisseur – créer, mettre à jour ou supprimer des ressources réelles. Par exemple, AWS, GCP, Azure, Dominos pizza, etc. (oui, il existe un fournisseur pour commander des Dominos Pizzas via Terraform !)
- Plugins Provisioner – Prend des mesures sur les ressources après la création, par exemple Ansible, remote-exec, fichier, etc.
Nous nous concentrons ici sur les plugins de fournisseur et l’ajout de ressources dans un fournisseur existant. Terraform a défini un mini SDK (partie antérieure du noyau, récemment séparé en un module go indépendant) qui définit une interface pour les ressources et quelques fonctions d’assistance pour faciliter le développement.
Chaque plugin fournisseur enregistre un schéma, une fonction de configuration et une liste de ressources et de sources de données avec le noyau. Chaque ressource, à son tour, se compose d’un schéma et d’un ensemble de fonctions CRUD. Terraform core utilise le schéma pour analyser les attributs de ressource du code, puis appelle les fonctions CRUD selon les besoins pour créer, mettre à jour, lire ou supprimer la ressource. Le SDK fournit de nombreuses fonctions utilitaires pour définir rapidement des ressources fiables.
Flux de travail
Avant de commencer le développement, assurez-vous que Golang 1.13 ou plus récent est installé et configuré correctement.
Le débit de haut niveau est répertorié ci-dessous :
- Fork le référentiel GitHub pour le plugin du fournisseur. Chaque plugin est maintenu dans son propre référentiel SCM dédié
- Cloner et extraire le code source dans votre espace de travail
- Ajouter une nouvelle ressource
- Inscrire la nouvelle ressource dans le fournisseur
- Ajouter des tests d’acceptation et s’assurer que tous les tests réussissent
- Ajouter de la documentation
- Transférer les modifications à votre référentiel forké
- Créer un PR vers le référentiel principal en suivant les instructions.
Ici, nous nous concentrerons sur les étapes 4, 5, 6 et 7.
Répertoires sources
Terraform core – https://github.com/hashicorp/Terraform
Providers – https://github.com/Terraform-providers
Pour développer une nouvelle ressource, nous n’avons besoin que du référentiel de code du fournisseur spécifique que nous améliorons, tout le reste est fourni. Dans cet article, il s’agit du fournisseur AWS et est présent à https://github.com/Terraform-providers/Terraform-provider-aws. Toutes les références de code source sont relatives à Terraform core ou au plug-in du fournisseur AWS.
Ressources
En règle générale, chaque référentiel de fournisseur contient un répertoire (package Golang) nommé d’après le fournisseur lui-même. Chaque ressource est définie dans son propre fichier source nommé selon la convention :
resource_<provider>_<resource name>.go
Nous allons utiliser l’exemple du filtre miroir de trafic AWS comme exemple et notre première tâche est de créer un nouveau fichier
aws/resource_aws_traffic_mirror_filter.go
Ensuite, nous créons une fonction d’initialisation privée (en Golang, cela signifie que le nom de la fonction commence par une petite lettre) nommée de manière appropriée –
func resourceAwsTrafficMirrorFilterRule() *schema.Resource
Cette fonction renvoie simplement une structure Golang de type `schema.Resource`. Ici `schema` est un pack défini dans le SDK du plugin Terraform et `Resource` est la structure de données représentant une ressource dans Terraform Voici un aperçu rapide du type de données ’Ressource’ (montrant uniquement les champs minimum requis, veuillez vous référer au code source pour la définition complète. Bien qu’il n’y ait pas beaucoup de documentation SDK, le code source est très bien commenté).
type Resource struct {
Schema map[string]*Schema
...
Create CreateFunc
Read ReadFunc
Update UpdateFunc
Delete DeleteFunc
...
}
REMARQUE : le mot « schema » apparaît ici avec plusieurs significations – nom du paquet dans le SDK, un type et un nom de variable.
Schéma
La variable « Schema » à l’intérieur de « Resource » est une carte dont les clés sont les noms des attributs pris en charge par la ressource et ses valeurs décrivent la nature de ces attributs. Le type « Schema » dans le SDK est utilisé pour décrire la nature des attributs des ressources de manière cohérente entre les fournisseurs.
Voici un aperçu rapide du type « Schema » avec les champs les plus courants
type Schema struct {
Type ValueType // doit être l’une des constantes définies dans ValueTypes
...
Optional bool // si l’attribut est facultatif
Required bool // si l’attribut est obligatoire
...
Elem interface{} // Lorsque le type est une liste, un ensemble ou une carte, cela indique la valeur
...
Description string
...
ForceNew bool
...
ConflictsWith []string // Une liste d’attributs est en conflit
ExactlyOneOf []string // Seuls les attributs de la liste peuvent être spécifiés
AtLeastOneOf []string // Une liste d’attributs dont au moins un est requis
...
ValidateFunc SchemaValidateFunc
…
}
Voici comment à quoi ressemble le schéma de la ressource filtre miroir de trafic :
Schema: map[string]*schema.Schema{
"description": {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
},
"network_services": {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
ValidateFunc: validation.StringInSlice([]string{
"amazon-dns",
}, false),
},
},
},
Et voici à quoi ressemblerait la définition de ressource correspondante dans Terraform:
resource "aws_traffic_mirror_filter" "filter" {
description = "test filter"
network_services = ["amazon-dns"]
}
Fonctions CRUD
Une fois que nous sommes prêts avec le schéma de la ressource, nous devons fournir les fonctions de création, de mise à jour, de lecture et de suppression (CRUD) de la ressource. Les fonctions sont définies dans le même fichier source que les fonctions privées et le pointeur/référence est stocké dans la structure « Ressource » dans les champs appropriés.
Un autre champ commun dans la structure « Ressource » est le champ « Importateur ». La définition de ce champ rend la ressource importable.
Enfin, la fonction d’initialisation du filtre miroir de trafic ressemblerait à ce qui suit :
func resourceAwsTrafficMirrorFilter() *schema.Resource {
return &schema.Resource{
Create: resourceAwsTrafficMirrorinFilterCreate,
Read: resourceAwsTrafficMirrorFilterRead,
Update: resourceAwsTrafficMirrorFilterUpdate,
Delete: resourceAwsTrafficMirrorFilterDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
... // contents of schema is shown earlier
},
}
}
Anatomie d’une fonction CRUD
Toutes les fonctions CRUD doivent avoir la signature suivante :
func (d *schema.ResourceData, meta interface{}) error
La fonction CRUD reçoit deux arguments lorsqu’elle est invoquée par le noyau et est censée renvoyer un objet d’erreur qui, en cas de succès, doit être défini comme « nil ». Les deux arguments sont expliqués en détail ci-dessous
- ResourceData – Contient les détails de la ressource, y compris la configuration analysée et l’état actuel. Cela a également un ensemble de fonctions utilitaires, dont certaines seront examinées assez bientôt.
- Le deuxième argument est une interface spécifique au fournisseur et doit être typographiée au type approprié avant d’être utilisée. Cela contient l’objet de connexion au fournisseur. Dans le cas d’un fournisseur AWS, il s’agit du type « AWSClient » et contient une connexion établie à AWS telle que définie dans la configuration du fournisseur.
Remarque : AWSClient possède un champ membre pour chaque type de client pris en charge par le kit SDK AWS. Si la nouvelle ressource ajoutée est gérée par un nouveau type de client, des tâches supplémentaires doivent être effectuées dans « aws/config.go » pour définir un nouveau champ pour le client et l’initialiser dans le cadre de la configuration du fournisseur.
Voici une brève description de la fonction utilitaire fournie par « ResourceData ».
- SetId(id string) – Définissez l’ID de la ressource dans l’état Terraform. Si la valeur de « id » est vide, cela détruirait la ressource
- Id() – Renvoie l’ID de la ressource.
- GetOk(attribute string) (interface{}, bool) – Renvoie la valeur de l’attribut dans la configuration Terraform et indique s’il a été défini ou non. La première valeur n’est valide que si la deuxième valeur renvoyée a la valeur true.
- Set(attribute string, value interface{}) – Définit la valeur d’un attribut dans l’état. La valeur doit correspondre au type de l’attribut défini dans le schéma.
- HasChange(attribute string) bool – Indique si la valeur de l’attribut a changé dans la configuration. Ceci est pratique dans la fonction de mise à jour pour identifier les attributs modifiés
- GetChange(chaîne d’attribut) (ancienne, nouvelle interface{}) – Ne doit être appelé que si HasChange() a renvoyé true, auquel cas cela renvoie les anciennes et nouvelles valeurs de l’attribut.
- Partial(bool) – Activer ou désactiver le mode d’état partiel. Lorsque cette option est activée, seules les clés spécifiées via la méthode SetPartial sont enregistrées dans l’état final. Cela peut être utile dans les cas où une ressource dans Terraform nécessite plusieurs appels d’API et est créée par étapes en évitant de détruire et de recréer en cas d’erreurs.
- SetPartial(chaîne d’attribut) – Faire en sorte qu’un attribut fasse partie de l’état partiel qui serait conservé. Cela n’a d’effet que lorsque le mode partiel est activé.
À un niveau élevé, la fonction CRUD suit le modèle ci-dessous –
- Récupérer l’objet de connexion fournisseur
- Récupérer les détails de la configuration/de l’état à partir de ResourceData
- Initialiser les structures d’entrée pour l’appel d’API du fournisseur et effectuer l’appel
- Mettre à jour l’état de la ressource
- Retour de la réussite/erreur.
Fonction Create – lit les attributs de ResourceData, appelle l’API du fournisseur pour provisionner la ressource et mettre à jour les attributs de la ressource créée dans ResourceData. Il convient de veiller à définir une valeur unique (du point de vue du fournisseur) pour la fonction Id(), car nous devrons récupérer les détails de la ressource dans la fonction de lecture en utilisant uniquement cette valeur. S’il n’y a pas de valeur unique pour la ressource qui peut s’identifier, il peut être nécessaire de créer un ID composite.
Fonction de mise à jour – Identifier les modifications de configuration et mettez à jour la ressource avec un impact minimal. Si la seule façon de mettre à jour une ressource est de détruire et de recréer, cette fonction pourrait être laissée vide et le noyau Terraform utiliserait destroy and create pour réaliser la mise à jour.
Fonction de lecture – Récupérer l’Id() de ResourceData et les derniers détails de la ressource auprès du fournisseur et mettre à jour resourceData.
Fonction Delete –Récupérer l’Id() de ResourceData et supprimer la ressource dans le fournisseur. En cas de succès, définir Id() sur vide.
À titre d’exemple, voici la fonction de création du filtre miroir de trafic :
func resourceAwsTrafficMirrorinFilterCreate(d *schema.ResourceData, meta interface{}) error {
conn := meta.(*AWSClient).ec2conn
input := &ec2.CreateTrafficMirrorFilterInput{}
if description, ok := d.GetOk("description"); ok {
input.Description = aws.String(description.(string))
}
out, err := conn.CreateTrafficMirrorFilter(input)
if err != nil {
return fmt.Errorf("Error while creating traffic filter %s", err)
}
d.Partial(true)
d.SetPartial("description")
d.Partial(false)
d.SetId(*out.TrafficMirrorFilter.TrafficMirrorFilterId)
return resourceAwsTrafficMirrorFilterUpdate(d, meta)
}
Enregistrement d’une nouvelle ressource
C’est facile, allez dans le fichier source ’aws/provider.go’. Recherchez une variable « ResourcesMap » dans la structure de données « Provider ». Il s’agit d’une carte avec des clés étant le nom de la ressource (comme ce serait le cas dans la configuration) et la valeur est la structure de données « Ressource » qui, par convention, est définie en appelant la fonction d’initialisation que nous avons définie.
Construction et essais
Terraform fournit un fichier make en tant que wrapper pour exécuter les commandes go build. Bien que le plugin puisse être construit directement à l’aide des commandes go, il est préférable d’utiliser le fichier make car cela réduit les risques d’échec de la construction dans CI lorsque nous créons une demande de pull. Exécutez les commandes suivantes dans le répertoire racine du dépôt des sources pour construire le plugin
$ make lint
$ make build
Le plugin serait placé dans le répertoire « $GOPATH/bin/ » avec le nom du fournisseur. Dans le cas d’AWS, il s’agirait de « Terraform-provider-aws ».
Maintenant, pour tester la nouvelle ressource, je préfère écrire la configuration Terraform et exécuter les commandes Terraform. C’est parce que je trouve que c’est plus rapide parce que les tests à l’aide du cadre de test d’acceptation prennent trop de temps car chaque étape devrait créer et détruire des ressources à chaque fois. Mais pour que cela fonctionne, nous devrions rendre le plugin visible à Terraform. Les étapes détaillées pour la découverte de plugins sont documentées ici – https://www.Terraform.io/docs/extend/how-Terraform-works.html. Ce que j’ai trouvé le plus simple est de copier le binaire que nous avons construit dans le répertoire où réside le code Terraform en suivant une convention de nommage. Par exemple
$ cd <Terraform config directory>
$ cp $GOPATH/bin/Terraform-aws-provider ./Terraform-provider-aws_v3.35.0_x4
$ Terraform init
$ Terraform plan -out apply.out
$ Terraform apply apply.out
Remarque : Terraform doit être réinitialisé pour utiliser le nouveau plugin, il est donc important d’exécuter « Terraform init ».
Tests d’acceptation
Hashicorp n’accepte aucune demande d’extraction qui n’implémente pas les tests d’acceptation appropriés pour les ressources ou les corrections de bugs. Terraform utilise le framework de test standard de Golang et a ajouté un ensemble de fonctions d’assistance pour faciliter l’écriture de cas de test. Pour chaque ressource, il doit y avoir des tests correspondants définis dans un fichier source dans le même répertoire avec la convention de dénomination suivante :
resource_<provider>_<resource name>_test.go
Par exemple, les tests pour la ressource miroir de trafic définie dans resource_aws_trraffic_mirror_filter.go doivent se trouver dans le fichier resource_aws_traffic_mirror_filter_test.go.
La structure de données « TestCase » dans le pack de ressources du SDK doit être utilisée pour définir un cas de test d’acceptation unique qui teste le cycle de vie de création/mise à jour/suppression d’une ressource. Read est testé implicitement dans le processus. Un seul test d’acceptation peut simuler plusieurs applications Terraform en séquence. SDK fournit également une fonction « ParallelTest() » pour déclencher le test. Voici quelques champs importants pour TestCase
- PreCheck – Une fonction de rappel pour vérifier les conditions préalables pour le cas de test. Au strict minimum, la fonction « testAccPreCheck() » d’aws/provider_test.go peut être réutilisée pour vérifier la connectivité au fournisseur. Si possible, une fonction personnalisée supplémentaire peut être implémentée pour vérifier rapidement que les informations d’identification fournies disposent des autorisations requises pour les ressources.
- Fournisseurs – Fournisseur à utiliser pour les tests. La plupart du temps, il suffirait de définir sur la variable globale « testAccProviders », ce qui est suffisant. Mais si le test nécessite plusieurs fournisseurs, les usines des fournisseurs devront être utilisées, mais elles ne sont pas couvertes ici en détail.
- CheckDestroy – Une fonction de rappel qui serait appelée à la fin du cas de test. La fonction est censée parcourir toutes les ressources de l’état et s’assurer qu’elles sont effectivement supprimées du fournisseur.
- Étapes – Liste des étapes de test qui crée le cas de test. Les TestSteps sont décrits plus en détail ci-dessous.
Chaque TestCase peut avoir plusieurs étapes, chaque étape correspondant à une action d’application Terraform. Étant donné que l’état Terraform est passé d’une étape de test à une autre, il peut être utilisé pour simuler des mises à jour/modifications des ressources. À la fin de l’ensemble du cas de test, l’infrastructure supprime toutes les ressources présentes dans l’état. Il n’est donc pas nécessaire de définir un cas de test pour la suppression, mais se fait plutôt en passant une fonction de rappel qui peut vérifier que toutes les ressources sont effectivement supprimées.
Chaque étape de test prend deux champs
- Config – Configuration Terraform qui définit la ressource testée. Framework effectue l’action d’application sur cette configuration et, lorsqu’il est prêt, appelle la fonction « Vérifier » pour vérifier.
- Vérifier – Une fonction de rappel qui vérifie que la ressource a été créée comme souhaité. Si la ressource n’est pas dans l’état souhaité, la fonction doit renvoyer une erreur.
Il existe de nombreuses fonctions utilitaires fournies pour vérifier l’état souhaité des ressources et voici quelques-unes des plus courantes –
- TestCheckNoResourceAttr(chaîne de ressource) – Vérifie que la ressource est présente dans l’état Terraform.
- TestCheckResourceAttr(chaîne de ressource, chaîne d’attribut, chaîne attendue) – Vérifie que les ressources nommées ont la valeur attendue pour l’attribut spécifié dans l’état Terraform.
- TestCheckNoResourceAttr(chaîne de ressource, chaîne d’attribut) – Vérifie que la ressource n’a pas l’attribut défini dans l’état Terraform.
- ComposeTestCheckFunc(fn … TestCheckFunc) – Assistant pour combiner plusieurs assertions en une seule fonction de vérification. La fonction accepte une liste de fonctions TestCheck* et renvoie une fonction qui renvoie une erreur en cas d’échec.
Le flux de test de base consiste à définir une configuration Terraform pour la ressource et à la passer
Un point important à garder à l’esprit en ce qui concerne les tests Terraform est que l’état est passé d’un TestStep à un TestStep suivant. Cela peut être utilisé pour tester des scénarios de mises à jour et de suppressions.
Voici le cas de test pour le filtre miroir de trafic :
func TestAccAWSTrafficMirrorFilter_basic(t *testing.T) {
resourceName := "aws_traffic_mirror_filter.filter"
description := "test filter"
resource.ParallelTest(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
testAccPreCheckAWSTrafficMirrorFilter(t)
},
Providers: testAccProviders,
CheckDestroy: testAccCheckAwsTrafficMirrorFilterDestroy,
Steps: []resource.TestStep{
//create
{
Config: testAccTrafficMirrorFilterConfig(description),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsTrafficMirrorFilterExists(resourceName),
resource.TestCheckResourceAttr(resourceName, "description", description),
resource.TestCheckResourceAttr(resourceName, "network_services.#", "1"),
),
},
// Test Disable DNS service
{
Config: testAccTrafficMirrorFilterConfigWithoutDNS(description),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsTrafficMirrorFilterExists(resourceName),
resource.TestCheckNoResourceAttr(resourceName, "network_services"),
),
},
// Test Enable DNS service
{
Config: testAccTrafficMirrorFilterConfig(description),
Check: resource.ComposeTestCheckFunc(
testAccCheckAwsTrafficMirrorFilterExists(resourceName),
resource.TestCheckResourceAttr(resourceName, "description", description),
resource.TestCheckResourceAttr(resourceName, "network_services.#", "1"),
),
},
{
ResourceName: resourceName,
ImportState: true,
ImportStateVerify: true,
},
},
})
}
Exécution des tests
L’exécution de tests d’acceptation Terraform crée des ressources réelles et peut coûter de l’argent, alors assurez-vous que le bon compte est configuré en définissant correctement AWS_PROFILE.
REMARQUE : si votre profil se trouve dans ~/.aws/config au lieu de ~/.aws/credentials, notez que le kit SDK AWS Golang ne charge pas le fichier de configuration par défaut. Vous devez définir explicitement AWS_SDK_LOAD_CONFIG=true
Le test peut être invoqué à partir du répertoire racine en utilisant la commande suivante :
$ make testacc TEST=./aws TESTARGS=”-run=TestAccAWSTrafficMirrorFilter”
Il existe une communauté dans gitter où vous pouvez chercher de l’aide au cas où vous rencontreriez des problèmes.
Documentation
Hasicorp exige également que la demande de retrait soit accompagnée d’une documentation appropriée pour les ressources. Pour une nouvelle ressource, nous devons d’abord ajouter le code html approprié dans le fichier « website/aws.erb ». Le moyen le plus simple consiste à copier les lignes correspondant à une ressource existante et à l’insérer à un endroit approprié (par ordre alphabétique) et à mettre à jour le texte. Ceci est utilisé par les outils CI pour générer la barre latérale que nous voyons dans la page de documentation de Terraform. Voici un exemple des modifications apportées aux ressources de mise en miroir du trafic :
Maintenant, nous devons également créer un nouveau fichier (un par ressource) qui correspond à l’hyperlien que nous avons mentionné sur la page. Il s’agit de documents markdown résidant dans le répertoire « website/docs/r/<resource name>.html.markdown ». Encore une fois, il est plus facile de copier un document existant et de le mettre à jour afin qu’il soit facile de se conformer aux exigences. Vous pouvez également exécuter la commande suivante pour effectuer quelques vérifications sur la documentation :
$ make docscheck
Demande Pull
Une fois que tous les tests sont réussis et que les documents sont prêts, vous pouvez créer une demande Pull de votre référentiel forké vers le référentiel d’origine. Il y a un ensemble de directives à suivre lors de la création des demandes de retrait et elles sont décrites ici :
https://github.com/Terraform-providers/Terraform-provider-aws/blob/master/.github/CONTRIBUTING.md#pull-requests
Références
- https://www.Terraform.io/docs/extend/index.html
- https://github.com/Terraform-providers
- https://github.com/hashicorp/Terraform-plugin-sdk
- https://github.com/hashicorp/Terraform
Découvrir plus de blogs de la communauté technologique Cloudreach ici .