Comment écrire un Provider Terraform ? Voici un exemple bout-en-bout avec l’API de Mattermost Partie 1 !

Mattermost est une application de chat, c’est une alternative open source et auto hébergée à Slack et Microsoft Teams. C’est l’application que nous utilisons en interne à OCTO Technology ! Et pour nous faciliter la vie, nous avons eu besoin d’ajouter automatiquement nos nouveaux arrivants et nouvelles arrivantes aux bons canaux, groupes… Et quoi de mieux qu’un provider Terraform pour ce faire ?

Vous pouvez le trouver et l’utiliser directement depuis la registry Terraform : https://registry.terraform.io/providers/octo-technology/mattermost/

Nous vous proposons de ce fait de vous partager la marche à suivre pour créer vos propres providers terraform !

Brève introduction à Terraform

Nous avons beaucoup abordé le sujet de terraform sur ce blog.

Pour faire simple, Terraform est défini dans leur description Github : “Terraform vous permet de créer, modifier et améliorer l’infrastructure de manière sécurisée et prédictible. C’est un outil Open Source qui va codifier des APIs en fichier de configuration qui peuvent être partagé avec d’autres membres de l’équipe, traité comme du code, édité, revu et versionné. Au départ dédié pour manager n’importe quelle infrastructure de Cloud, Terraform est vite devenu un outil capable d’abstraire n’importe quelle API de façon déclarative.

On définit via du code les ressources désirées sur n’importe quel fournisseur (un “provider”, AWS, Azure, Gitlab, … Mattermost) et Terraform sera chargé d’appeler les API nécessaires pour que votre rêve devienne réalité.

source : https://www.terraform.io/

Que se cache-t-il derrière la notion de “provider” ?

Terraform peut être divisé en deux briques :

  • Le cœur
  • Les plugins

Le cœur de Terraform est responsable de la génération du graphe de dépendances entre toutes les ressources. Il a pour rôle de parser l’ensemble du code HCL (Hashicorp Configuration Langage) utilisé pour définir notre infrastructure, puis créer un graphe liant tous les blocs entre eux. Par exemple, c’est lui qui déterminera qu’un sous-réseau doit être créé après le réseau dans lequel il doit apparaître.

Il va également fournir un ensemble de points d’entrées machines humains (IHM), tels que la CLI et le langage lui-même.

Il fournit par ailleurs une interface commune et tous les plugins auront à implémenter cette interface et le cœur communiquera avec les différents plugins existants.

Un Provider Terraform est en fait un plugin (comme expliqué pour le provider AWS), qui communique avec l’API cible : Cloud Provider, API, ou même votre machine à café si vous voulez !

Un plugin est un serveur gRPC écrit dans le langage que vous souhaitez (bien que le SDK disponible soit écrit en Golang). Il va spécialiser Terraform en intéragissant avec l’API cible. Ce plugin et le cœur de terraform communiqueront via un protocole construit autour de gRPC.

Datasources et Resources dans Terraform

Resources

La déclaration d’une ressource dans Terraform va déclencher la gestion par Terraform d’un objet correspondant sur votre Provider. Par exemple, lorsque l’on déclare une ressource `aws_lb`, l’objet correspondant sera une instance de load-balancer sur AWS.

La marche à suivre consiste donc pour nous à examiner l’API Mattermost, voir ce qui nous intéresse et l’implémenter dans notre Provider Terraform sous forme de resources.

Datasources (Sources de données)

Une Datasource est un moyen d’obtenir des informations sur des objets définis en dehors de Terraform. Il s’agit d’une ressource en lecture seule qui peut être référencée dans d’autres Resources Terraform ou Datasource, ce qui permet de ne pas coder en dur les valeurs dans notre code.

Par exemple, si vous voulez instancier une VM dans un sous-réseau qui n’est pas géré par vous, vous allez alors utiliser un Datasource de ce sous-réseau, que vous allez référencer dans votre “resource VM”.

TDI? Tests Driven Infrastructure!

L’un des piliers d’une bonne méthodologie DevOps passe par l’Infrastructure As Code. Notre infrastructure est décrite sous forme de code et peut donc être versionnée et testée. Comme c’est du code, il nous est possible d’appliquer les bonnes pratiques du monde du développement.

Nous allons donc tant que faire se peut, suivre les principes du TDD. Ou dans notre cas, le développement d’infrastructures piloté par les tests !

Entrons dans le vif du sujet !

Pour Terraform, nous avons le choix entre le nouveau et l’ancien SDK :

Plugin Development: Which SDK Should I Use? | Terraform by HashiCorp.

Vous pouvez trouver l’ancien SDK V2 et le nouveau SDK Framework.

Vous pouvez aussi combiner les deux, le temps de migrer de l’ancien au nouveau par exemple. La couverture de cette migration donnera lieu à des articles supplémentaires, c’est d’ailleurs pour cela que nous partirons sur l’ancien SDK dans un premier temps.

Un exemple d’un tel Provider est disponible sur GitHub.

Vous pouvez même vous passer tout bonnement de SDK pour répondre à des cas avancés.

Le choix entre l’ancien SDK et le nouveau se fera de la manière suivante :

  • Le cas ou vous maintenez déjà un provider sur l’ancien SDK
  • L’ancien SDK est toujours maintenu
  • La migration vers le nouveau SDK est possible avec terraform-plugin-mux
  • Le cas ou vous voulez développer un nouveau provider from scratch
  • Dans ce cas il ne faut pas hésiter à partir sur le nouveau SDK
  • Le framework ne supporte pas les version Terraform CLI inferieur a la 1.0
  • Si vos utilisateurs n’ont pas encore une version de Terraform superieur a la 1 vous devriez rester sur le SDK V2
  • A note que le Framework n’est pas stable, cela étant dit l’API va évoluer et très certainement rencontrer des breaking changes (Le debugger n’est pas implémenté dans le Framework pour l’instant par exemple)
  • Le Framework offre une multitude de nouvelles fonctionnalités

Il est aussi possible de partir de rien, mais en bon développeur nous sommes fainéants et si nous pouvons nous épargner du travail…

Clonons le répertoire de code et regardons rapidement ce qu’il contient :

  • Parce que c’est une application écrite en Go, nous avons l’habituel main.go qui sera le point d’entrée de notre Provider, nous n’aurons pas à le modifier ;

  • Les go.mod, go.sum, où seront ajoutés respectivement la liste de nos dépendances Go et leurs signatures, par exemple le SDK Mattermost sera présent ;

  • Le GNUmakefile, un Makefile simple avec les commandes d’exécution des tests, la génération de la documentation, etc.

  • Le dossier outils qui contient un fichier Go avec le nécessaire pour générer la documentation ;

  • Le dossier exemple, où nous pouvons mettre différents exemples d’utilisation de notre Provider, cela peut aider les utilisateurs à l’utiliser rapidement en complément de la documentation ;

  • Le dossier docs contenant la documentation générée pour notre Provider ;

  • Et le plus important, dans lequel nous passerons la majorité de notre temps, le dossier internal/provider.

Notre point d’entrée main.go va appeler la fonction New() de notre package provider. C’est la seule chose que nous ayons besoin de savoir.

Il y a déjà du code simple dans le dossier internal/provider.

Dans le provider.go, vous trouverez la définition des différentes Datasources et Resources que nous implémenterons après dans notre Provider :


func New(version string) func() *schema.Provider {
    return func() *schema.Provider {
            p := &schema.Provider{
                    DataSourcesMap: map[string]*schema.Resource{
                            "scaffolding_data_source": dataSourceScaffolding(),
                    },
                    ResourcesMap: map[string]*schema.Resource{
                            "scaffolding_resource": resourceScaffolding(),
                    },
            }
            p.ConfigureContextFunc = configure(version, p)
            return p
    }
}

Ici, il y a une relation entre le fichier nommé dans le répertoire et les Resources et Datasources déclarés dans le schéma du Provider, dans le code retourné par la fonction “New” :

  • scaffolding_data_source
  • scaffolding_resource

Et les tests qui vont avec.

Définissons la configuration du Provider

Nous avons parlé des tests en premier, oui, mais dans ce cas, nous avons juste à renommer le nom du Provider dans la factory de ce dernier dans le fichier provider_test.go :


var providerFactories = map[string]func() (*schema.Provider, error){
    "mattermost": func() (*schema.Provider, error) {
            return New("dev")(), nil
    },
}

Nous voulons communiquer avec un serveur Mattermost, pour ce faire, nous devons spécifier une configuration dans le bloc Provider. Cette configuration va inclure l’URL du serveur et un token d’API à utiliser pour l’auth/authz


func New(version string) func() *schema.Provider {
    return func() *schema.Provider {
            p := &schema.Provider{
                    Schema: map[string]*schema.Schema{
                            "url": {
                                    Type:            schema.TypeString,
                                    Required:        true,
                                    DefaultFunc: schema.EnvDefaultFunc("MM_URL", nil),
                                    Description: "Can also be provided via the MM_URL environment variable",
                            },
                            "token": {
                                    Type:             schema.TypeString,
                                    Optional:         true,
                                    DefaultFunc:  schema.EnvDefaultFunc("MM_TOKEN", nil),
                                    Description:  "Can also be provided via the MM_TOKEN environment variable",
                            },
                    },
            }

            p.ConfigureContextFunc = configure(version, p)

            return p
    }
}

Pour ce faire, nous allons modifier les paramètres que doit prendre notre Provider. La définition des paramètres correspondant est définie dans son schéma. Dans notre cas, l’URL et le Token du serveur Mattermost avec lequel nous désirons communiquer.


provider "mattermost" {
  url          = "http://localhost:8080"
  token = "xxxx"
}

L’appel de notre Provider en langage HCL se fera comme ci-dessus.

Mais voulons-nous vraiment passer nos informations de connexions directement dans notre code Terraform ? Pas réellement, donc la partie Optional et DefaultFunc de notre schéma nous donnera la flexibilité de passer ces informations comme variables d’environnement.


export MM_URL="http://localhost:8080"
export MM_TOKEN="xxxxxxxxxxxxxxxxxxx"

provider "mattermost" {
}

Bien mieux !

Il est important de ne pas oublier d’installer et d’importer le SDK Mattermost :


go get -u github.com/mattermost/mattermost-server/v6/model

Et normalement l’environnement de développement de votre choix devrait être assez intelligent pour porter automatiquement les dépendances lorsque c’est nécessaire merci à Gopls.


import (
    "context"

    "github.com/hashicorp/terraform-plugin-sdk/v2/diag"
    "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
    "github.com/mattermost/mattermost-server/v6/model"
)

La modification du schéma lui-même est une première étape, mais l’implémentation réelle sera dans la fonction configure du fichier provider.go :


func configure(version string, p *schema.Provider) func(context.Context, *schema.ResourceData) (interface{}, diag.Diagnostics) {
    return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
            url := d.Get("url").(string)
            token := d.Get("token").(string)
            c := model.NewAPIv4Client(url)
            c.SetOAuthToken(token)

            return c, nil
    }
}

Le schéma, que nous avons défini au préalable, doit être passé à la fonction configure :


p.ConfigureContextFunc = configure(version, p)

C’est ici que toute la magie se passe, d’abord nous récupérons les variables, token, URL : celles-ci doivent être castées en type string go.

Ensuite nous pouvons initialiser le client Mattermost, ce client sera utilisé pour chaque appel API que nous aurons à effectuer après. Le client est initialisé avec l’URL, on lui passe aussi le Token. Ensuite nous pouvons retourner le client, le retour de la fonction accepte une interface, donc tout est bon !

Conclusion

Après quelques petits rafraîchissements, sur le fonctionnement de Terraform et l’écosystème autour des providers, ce dernier est désormais configuré.

Dans les prochaines parties, nous commencerons à implémenter notre première resource. Stay tuned.

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.


Ce formulaire est protégé par Google Recaptcha