test d'acceptance pour provider terraform

temps de lecture ~11 min
  1. 1. Un provider simple: wait
  2. 2. Le fonctionnement du provider
    1. 2.1. Init
    2. 2.2. Plan
    3. 2.3. Apply
    4. 2.4. Show
  3. 3. Les tests d’acceptance
    1. 3.1. Exemple simple
    2. 3.2. Exemple à plusieurs étapes
  4. 4. Exécution des tests d’acceptance
  5. 5. Dans un processus d’intégration continu (CI)
  6. 6. Conclusion
  7. 7. à voir aussi

Dans le monde des orchestrateurs, terraform propose un écosystème insteressant et surtout extensible. Si vous ne trouvez pas votre bonheur dans l’ensemble des possibilités de l’outils ou si vous proposez un service pas encore connecté, vous pouvez alors étendre les capacités de l’outil. C’est ce que l’on appelle un provider.

Ecrire un provider n’est pas spécifiquement complexe dès lors que l’on a compris la philosophie générale de l’outil. terraform étant idempotent (de multiples exécutions fournissent le même résultat) on comprend pourquoi il faut câbler des méthodes de vérification avec celles de création, modification et destruction. Cela permet de faire le lien entre le monde que l’on souhaite piloter à traver le provider et l’état que terraform conserve, les deux n’étant pas synchrones, il est nécessaire d’effectuer des mises à jour avant et après les opérations.

Vous trouverez beaucoup de documentation sur comment écrire un provider pour terraform et surtout il y a beaucoup de modules distribués en opensource. Chez EfficientIP nous publions un provider permettant de piloter une partie significative de notre solution DDI (DNS-DHCP-IPAM) et comme nous utilisons cet outil très régulièrement, nous l’enrichissons au fur et à mesure que des besoins se créent.

Lorsque l’on travaille sur ce type de développement, en plus des tests unitaires qui sont souvent mis en oeuvre, des tests d’acceptance peuvent être intéressants. Ils permettent de tester en situation le fonctionnement du provider sans pour autant avoir à manipuler des fichiers de description. L’objet de ce billet est de vous montrer comment mettre en place ces tests.

Un provider simple: wait

Pour vous montrer comment intégrer des tests d’acceptance dans terraform, je vous propose de partir sur la base d’un provider que j’ai écrit à cette occasion, vous le trouverez sur mon gitlab : terraform-provider-wait. Il permet de mettre une pause dans votre configuration d’orchestration. Pas forcément très utile, mais assez simple à comprendre.

La structure du provider est basé sur les quelques éléments suivants (dans le répertoire wait) :

  • provider.go : la définition du provider lui-même, ses datasources et ses resources, ici le point important est de lier wait_sleep qui sera utilisé dans les fichiers terraform et la fonction resourcesleep qui se trouve dans resource_sleep.go.
  • wait.go : le fichier permettant de construire un état, le lien avec le monde à relier, ici, pas grand chose d’autre qu’une variable permettant de construire des objets avec un identifiant différent à chaque construction. Je vous ai mis une version pour voir comment elle est utilisable par la suite au sein de terraform.
  • resource_sleep.go : ici on définit tout ce qui est nécessaire pour brancher notre ressource avec l’ensemble des fonctions qui vont être utilisées au cours du cycle de vie : création, modification, destruction ainsi que le lien avec le monde extérieur. On y définit également les paramètres qui pourront être utilisés à partir du fichier de configuration de terraform, ici uniquement le délai d’attente que l’on souhaite et la valeur calculée (version) juste pour l’exemple.

Le fonctionnement du provider

Avant de détailler le fonctionnement des tests d’acceptance, regardons comment s’utilise ce provider et le format des fichiers de configuration terraform, nous utiliserons les même dans les tests.

test.tf
1
2
3
4
5
6
7
8
9
provider "wait" {}
resource "wait_sleep" "ex01" {
delay = 1000
}
output "ex01" {
value = "id:${wait_sleep.ex01.id} version:${wait_sleep.ex01.version} sleep:${wait_sleep.ex01.delay}ms"
}
  • ligne 8 : utilisation des informations contenues dans la ressource.

Init

shell
1
2
3
4
5
$ terraform init
Initializing provider plugins...
Terraform has been successfully initialized!

Cette action permet de charger le provider dans l’environnement d’exécution, à refaire à chaque modification de celui-ci.

Plan

shell
1
2
3
4
5
6
7
8
9
$ terraform plan
Terraform will perform the following actions:
+ wait_sleep.ex01
id: <computed>
delay: "1000"
version: <computed>
Plan: 1 to add, 0 to change, 0 to destroy.

Apply

shell
1
2
3
4
5
6
7
8
9
10
11
$ terraform apply -auto-approve
wait_sleep.ex01: Creating...
delay: "" => "1000"
version: "" => "<computed>"
wait_sleep.ex01: Creation complete after 1s (ID: 1)
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Outputs:
ex01 = id:1 version:1.0 sleep:1000ms

Show

shell
1
2
3
4
5
6
7
8
9
$ terraform show
wait_sleep.ex01:
id = 1
delay = 1000
version = 1.0
Outputs:
ex01 = id:1 version:1.0 sleep:1000ms

Les tests d’acceptance

Pour la partie acceptance, un point important à intégrer est que ces tests sont actifs et donc potentiellement connectés à votre élément piloté. Vous devez donc avoir votre environnement actif ou à minima bouchonné afin d’avoir des résultats cohérents, ce n’est pas uniquement des tests en vase clos.

Le principe à suivre :

  • pour chaque ressource ou connecteur de données (datasource) on crée un fichier de test qui sera autonome
  • on le tag avec des paramètres de compilation permettant de n’exécuter qu’une partie des tests si besoin (cf build & -tags / -run)
  • on découpe les tests en plusieurs fonctions préfixées par TestAcc, ici on pourra ajouter des tests passants, des bloquants, des évolutions, aucune limite
  • chaque test peut être découpé en étapes (steps) qui vont constituer les différentes évolutions de votre fichier terraform, ceci permet de valider si les modifications apportées à la configuration sont correctement appliquées sur l’élément piloté
  • chaque test peut être conditionné, disposer de tests préalables et post exécution.
  • chaque étape peut être suivi de test sur la présence de paramètre sur la ressource mais aussi d’en valider le contenu
  • chaque test est associé à un fichier de configuration au format terraform

Par habitude, je construis les configurations terraform dans une ou plusieurs fonctions, ce ne sont que des chaînes de caractères, des paramètres pourront être utilisés afin de réduire le nombre de ces fonctions.

Exemple simple

resource_sleep_test.go
1
2
3
4
5
6
7
func Config_TestAccSleep_01(name string, delay int) string {
return fmt.Sprintf(`
resource "wait_sleep" "%s" {
delay = %d
}
`, name, delay)
}

Ici on dispose d’une fonction pour créer un fichier de terraform ressemblant à :

test.tf
1
2
3
resource "wait_sleep" "test01" {
delay = 1000
}

Dans le test, on va demander à terraform de charger notre provider, d’appliquer la configuration et de valider que tout c’est bien passé. Le code extrait est le suivant :

resource_sleep_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func TestAccSleep_01(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
Providers: testProviders,
Steps: []resource.TestStep{
{
Config: Config_TestAccSleep_01("test", 10),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("wait_sleep.test", "id"),
),
},
},
})
}
  • 4 : on applique un test global, ici vide et présent dans provider_test.go, ceci pourrait permettre de conditionner le contexte du test
  • 9 : on fabrique la configuration
  • 11 : on valide qu’un id a bien été crée pour cette ressource

Exemple à plusieurs étapes

Ici on effectue une modification de la configuration afin de valider que les étapes de création et de modification effectuent bien les effets escomptés :

resource_sleep_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Steps: []resource.TestStep{
{
Config: Config_TestAccSleep_01("test", 10),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("wait_sleep.test", "id"),
resource.TestCheckResourceAttr("wait_sleep.test", "delay", "10"),
),
},
{
Config: Config_TestAccSleep_01("test", 20),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttrSet("wait_sleep.test", "id"),
resource.TestCheckResourceAttr("wait_sleep.test", "delay", "20"),
),
},
},
  • 3 : création de la première version de la configuration avec un délai positionné à 10ms
  • 5 : on valide la création
  • 6 : on valide que le paramètre est bien pris en compte
  • 10 : on change la configuration de la même ressource en passant le délai à 20ms
  • 12 + 13 : on valide le résultat

Exécution des tests d’acceptance

Les fichiers *_test.go ne seront utilisés que pendant la phase de test d’acceptance, ils ne seront même pas compilés lors d’un build.

Afin d’exécuter les tests on positionne la variable d’environnement TF_ACC à 1, en ligne de commande on aura quelque chose comme :

shell
1
2
3
4
5
6
7
8
9
$ TF_ACC=1 go test gitlab.com/achauvinhameau/terraform-provider-wait/wait -v -tags="all"
=== RUN TestAccSleep_01
--- PASS: TestAccSleep_01 (0.07s)
=== RUN TestAccSleep_02
--- PASS: TestAccSleep_02 (1.06s)
=== RUN TestAccSleep_03
--- PASS: TestAccSleep_03 (0.11s)
PASS
ok gitlab.com/achauvinhameau/terraform-provider-wait/wait 1.256s
  • 1 : on teste tous les modules de notre code, on pourrait limiter avec un -tags=“sleep” par exemple
  • 2 à 7 : l’ensemble des tests, on pourrait limiter en ajoutant -run “Sleep_02” par exemple
  • 8 : le résultat global
  • 9 : le temps

Vous pouvez également positionner la variable d’environnement TF_LOG à INFO ou DEBUG pour avoir un peu plus d’information à l’exécution.

Dans un processus d’intégration continu (CI)

Enfin, si vous souhaitez disposer de ce type de test dans votre processus d’intégration continu, il pourra être nécessaire de faire quelques ajustements en fonction de votre moteur. Par exemple avec un gitlab, vous pourrez utiliser un fichier .gitlab-ci.yml avec les éléments suivants :

.gitlab-ci.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
image: alpine
stages:
- build
before_script:
- apk add --no-cache --virtual .build-deps go git libpthread-stubs libc-dev
- mkdir go gobin
- export GOPATH=/terraform-provider-wait/go
- export GOBIN=/terraform-provider-wait/gobin
- go get
build:
stage: build
script:
- go fmt . ./wait/
- go build
- TF_ACC=1 go test gitlab.com/achauvinhameau/terraform-provider-wait/wait -v -tags="all"

Conclusion

Vous utilisez terraform ou vous travaillez sur un outil qui aurait tout intérêt à participer à une démarche de type “infrastructure-as-code”, proposer un provider est probablement incontournable et mettre en place une démarché qualité avec du test unitaire (couverture, qualité, lint, …) et du test d’acceptance sera apprécié par vos utilisateurs. Par ailleurs, ceci vous permettra également de disposer d’une base afin d’accepter des pull request de qualité car ayant passé l’ensemble des tests déjà présents, c’est intéressant de savoir que l’on a rien cassé.

Enfin, cette approche peut également avoir un réel intérêt afin de valider la non régression des évolutions apportées au composant adressé par ce provider. En cas d’évolution majeure ceci permettra de faire évoluer la partie terraform et dans tous les cas de tester que l’on n’a rien cassé dans le produit. Les environnements de tests ayant tendance à être de plus en plus complexes, notamment lorsque l’on manipule du code d’infrastructure, toute automatisation, même partielle est bonne à prendre.

Photo Davide Cantelli

Alexandre Chauvin Hameau

2019-04-28T17:49:24