Vos tests d'infrastructure de bout-en-bout avec Kitchen
Au cours de précédents articles, nous vous avons introduit le Test Driven Development sur du code d’infrastructure avec des outils tels que Molecule ou Terratest. Dans cet article, nous vous présenterons Kitchen-CI, un outil qui permet, avec l’aide de bibliothèques de test comme InSpec ou ServerSpec, de tester les différentes briques de son infrastructure.
Kitchen
Kitchen-CI (de son vrai nom) est un outil écrit en Ruby par les développeurs de Chef. À l’aide de plugins spécifiques, il permet d’effectuer des tests pour vérifier la conformité des ressources générées par notre code.
Grâce à Kitchen, il est possible de tester le résultat de l’exécution de tout un panel d’outils tels que Terraform ou Ansible. Cela permet d’avoir une boucle de retour automatisée qui est plus efficace qu’une vérification manuelle et de n’avoir qu’un seul outil pour tester toutes les briques de déploiement de notre infrastructure.
Un outil modulaire
La plus grande force de Kitchen-CI est sa modularité. Il dispose de divers modules lui permettant de s’adapter à la plupart des plateformes cloud (Amazon Web Service, Microsoft Azure…) et de virtualisation (Docker, Vagrant…) afin de mettre en place une infrastructure, la configurer et la tester.
Image tirée du site de Kitchen-CI
Fonctionnement de Kitchen
Le fonctionnement de Kitchen est très proche de celui de Molecule. Il se décompose en six étapes qui sont toutes jouées lors de l'exécution de la commande kitchen test :
Il faut savoir qu’il est possible de jouer chacune de ces étapes individuellement à l’aide des commandes :
**$** kitchen create **$** kitchen converge **$** kitchen setup **$** kitchen verify **$** kitchen destroy
Il faut également savoir que, contrairement à Molecule, Kitchen n’embarque pas nativement de bibliothèque de tests comme TestInfra. Par conséquent, il faut en ajouter une manuellement. Parmi les plus utilisées, on retrouve notamment InSpec et ServerSpec, deux bibliothèques mise en avant par les équipes de Chef dans la documentation et très appréciés par la communauté (respectivement 1638 et 2219 Stars sur leurs Github respectifs à l'heure où ces lignes sont écrites).
Étude de cas : Mise en place d’un serveur NGINX sur une infrastructure Azure
Pour illustrer le fonctionnement de Kitchen, essayons de construire l’architecture suivante sur Azure en testant à la fois la création de l’infrastructure (Terraform) et la configuration de la machine (Ansible) avec Kitchen :
Vous trouverez à cette adresse un dépôt git qui contient l’intégralité du projet. Essayons de comprendre comment chaque brique a été développée.
Explication de la partie Terraform
Voyons maintenant comment fonctionne Kitchen avec Terraform. Pour cela, je vous propose de jeter un œil au dossier terraform présent dans le dépôt git.
Présentation d’un exemple de code utilisant Terraform et Kitchen
Lorsque l’on regarde l’architecture d’un projet Terraform qui utilise Kitchen, on obtient généralement une architecture qui ressemble à ça :
Jetons maintenant un œil aux fichiers nécessaires au bon fonctionnement de Kitchen afin de comprendre son fonctionnement.
Tout d’abord, le fichier kitchen.yml. C’est lui qui permet à Kitchen de connaître le module qu’il devra utiliser lors de chacune des grandes étapes de son fonctionnement (create, converge, verify…). Ouvrons le fichier et regardons son contenu : --- driver:
name: **terraform**
root_module_directory: **test/tf_module**
variable_files:
- **test/tf_module/testsuit.tfvars**
provisioner:
name: **terraform**
verifier:
name: **terraform**
systems:
- name: **azurerm**
backend: **azure**
controls:
- **resource_group**
- **network-vnet**
- **network-subnet**
- **virtual-machine**
platforms:
- name: **terraform**
suites:
- name: **default**
On y retrouve cinq grandes catégories :
- driver : Kitchen utilise les informations présentes dans cette partie afin de mettre en place l’infrastructure. Ces informations sont principalement utilisées lors des phases de création et de destruction. Dans cette partie, nous retrouvons la configuration nécessaire à l’exécution du code terraform mais nous y reviendrons un peu plus bas.
- provisioner : Cette partie permet de faire converger la configuration des machines vers l’état attendu (exécution du code Ansible). Dans le cas de terraform-kitchen, cette partie n’a pas grand intérêt.
- verifier : C’est ici que nous retrouvons les informations liées à la phase de test (verify).
- platforms : Cette partie sert à définir les spécificités des environnements de tests lorsque l’on utilise des modules tels que Vagrant. Par exemple, on pourrait définir l’OS de la machine, son nom etc… Dans le cas de kitchen-terraform, cette partie est peu importante.
- suites : On y retrouve les diverses suites de tests qui seront utilisées par Kitchen. Ici, on peut voir qu’on utilise uniquement la suite de tests nommée default.
On retrouve ensuite le fichier chefignore qui est utile si Kitchen est utilisé avec Chef afin de spécifier quel fichier doit être ignoré lors de l’envoi de donnée à un serveur Chef. Dans notre cas, ce fichier n’est pas utile.
Enfin, on retrouve le dossier test. C’est lui qui contient tout ce dont Kitchen a besoin pour jouer ses tests. Dans celui-ci, nous retrouvons tout d’abord le dossier integration qui contient toutes les suites de tests. Comme dit plus haut, Kitchen ne dispose pas de bibliothèque de tests native telle que TestInfra. Il faut donc en mettre une en place. Dans cet exemple, Inspec a été utilisé afin de mettre en place de pouvoir écrire des tests.
Inspec est une bibliothèque créée par les équipes de Chef afin de pouvoir tester son infrastructure. Elle dispose d’un nombre grandissant de ressources de test sur les principaux cloud provider (telle que Amazon Web Service, Google Cloud Platform et Microsoft Azure).
En utilisant Inspec, plusieurs fichiers sont importants :
Le fichier inspec.yml qui contient les informations relatives au cloud provider que l’on souhaite utiliser. Dans notre cas, avec Microsoft Azure, nous voulons utiliser inspec-azure. Afin de l’utiliser, le fichier doit ressembler à ça : ---
name: **test-infrastructure**
title: **Le nom de ma suite de test**
version: 0.1.0
inspec_version: '>= 2.2.7'
depends:
- name: **inspec-azure**
url: https://github.com/inspec/inspec-azure/archive/master.tar.gz
supports:
- platform: **azure**
On trouve ensuite les controls. Il s’agit de l’endroit où vont se situer nos différents tests. Un des avantages d’InSpec est que les tests sont écrit en Ruby ce qui leur accorde une grande lisibilité. Prenons par exemple d’un test du fichier network.rb : control **'network-vnet'** do
title **'Validation of the VNet properties.'**
desc **"The vnet is created and is placed in Amsterdam region."**
describe azurerm_virtual_network(**resource_group: 'KitchenDemoEnvironment'**, **name: 'MyVnet'**) do
it { should exist }
its(**'location'**) { should eq **'westeurope'** }
end
end
On retrouve le nom du control qui a été déclaré dans le verifier du fichier kitchen.yml, ainsi que les tests qui lui sont associés.
Comment kitchen-terraform effectue-t-il ses tests ?
Maintenant que nous avons vu comment le code est organisé, voyons comment kitchen-terraform lance les tests ?
Dans notre dépôt, le code Terraform qui correspond à notre infrastructure correspond aux trois fichiers suivants qui se trouvent à la racine du dossier Terraform :
- topology.tf qui contient le code de l’infrastructure
- variables.tf qui contient les déclarations des variables de notre code Terraform.
- terraform.tfvars qui contient les valeurs des variables pour notre infrastructure principale.
On a ensuite un dossier test/tf_module qui contient la déclaration du module pointant sur le code que Kitchen va tester. À l’aide de cette interface et du fichier de variable spécifique présent dans le même dossier, il génère une infrastructure spécifique pour l’exécution des tests.
Dans le code de l’exemple, nous avons choisi d’effectuer les tests dans un autre ResourceGroup afin que cela n’ait pas d’impact sur l’environnement principal. On obtient donc un fonctionnement qui ressemble à ça :
On se retrouve donc avec 2 flux qui permettent la mise en place de deux environnements différents. Le premier flux est celui de Kitchen qui effectue les tests sur une infrastructure temporaire qui est détruite à chaque exécution de Kitchen. Une fois la validation effectuée, on peut finalement appliquer le changement sur un environnement qui a vocation à être plus stable (zone de test de développeur, pré-production...).
Si on veut appliquer ça au Test Driven Development, notre cycle de développement se décompose en 4 étapes :
Ce genre de cycle de développement répond à des objectifs de déploiement continu et on se retrouve ainsi à déployer une infrastructure fonctionnelle bien plus souvent et de manière plus sécurisée.
Explication de la partie Ansible
Maintenant que nos machines ont été créées, on cherche à installer notre serveur Nginx dessus. Pour ce faire, on utilise Ansible pour configurer notre machine créée précédemment et nous allons utiliser le module kitchen-ansible pour effectuer nos tests sur cette partie.
Jetons un œil au dossier ansible dans le dépôt git.
Présentation d’un exemple de code utilisant Ansible et Kitchen
Tout d’abord, regardons l’architecture du dossier :
Nous retrouvons l’architecture classique d’un dépôt Ansible. Notre dossier inventories contient un inventaire kitchen-demo avec les caractéristiques de la machine déployée lors de la partie Terraform. On retrouve également un playbook deploy.yml qui joue des rôles (présent dans le dossier éponyme). Enfin, notre but étant d’installer Nginx sur la machine créée plus tôt, nous avons un rôle install-nginx-server.
Dans le cas où nous avons un rôle à tester, Kitchen a un comportement similaire à celui de Molecule. Voyons comment celui-ci fonctionne. Le premier fichier que nous allons vouloir voir est le fichier kitchen.yml. Comme pour la partie Terraform, celui-ci comprend les instructions permettant à Kitchen d’effectuer ces tests. ---
driver:
name: **azurerm**
subscription_id: **'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'**
location: **'West Europe'**
machine_size: **'Standard_B2s'**
provisioner:
name: **ansible_playbook**
hosts: **all**
require_chef_for_busser: false
playbook: **kitchen-playbook.yml**
idempotency_test: true
verifier:
name: **inspec**
platforms:
- name: **ubuntu-18.04**
driver:
image_urn: Canonical:UbuntuServer:18.04-LTS:latest
vm_name: **Kitchen-Test-Machine**
vm_tags:
Purpose: **KitchenDemo**
suites:
- name: **default**
On retrouve ici les cinq parties vu précédemment avec quelques changements :
- Le driver passe de terraform à azurerm. C’est le driver qui se charge de mettre en place une machine de type Standard_B2s.
- On retrouve également un champ platforms un peu plus étoffé que lors de la partie Terraform. Dans celle-ci, on retrouve les informations relatives à la machine telles que son nom, l’image qu’on lui applique ou ses tags. On peut également spécifier les resourcegroup/vnet/subnet dans lequel on souhaite déployer l’instance.
- Le provisioner devient ansible_playbook car on souhaite tester ce que Ansible met en place sur nos machines (dans notre cas, le rôle). Dans celui-ci, on peut y définir le playbook qui est testé, tester l’idempotence…
- Enfin, on retrouve le verifier qui utilise Inspec et suites qui ne change pas par rapport à précédemment.
Contrairement aux tests avec Terraform, on a pas besoin de spécifier à InSpec comment jouer ces tests. La vérification se fera à l’aide des informations définies dans la partie platforms et il ira automatiquement récupérer les tests présents dans le dossier tests/integration/default/. Comme pour la partie Terraform, on retrouve des tests écrits en Ruby :
describe package(**'nginx'**) do
it { should be_installed }
end
Comment kitchen effectue-t-il ses tests ?
Pour l’exécution de tests, il procède de la même façon que Molecule. Il utilise le playbook kitchen-playbook.yml pour exécuter le rôle sur l’environnement de tests lors de la phase de configuration des machines (converge). Une fois fait, il joue les tests sur l’environnement puis le détruit.
On obtient donc un cycle de développement en 4 étapes similaires qui est le même que celui de kitchen-terraform. La seule différence réside dans la construction de l’infrastructure qui a lieu avec azurerm et qui se fait via un ResourceGroup dédié.
Dans notre exemple, nous aurions pu rajouter également une série de tests Inspec sur le playbook situé à la racine du dossier Ansible pour valider que nos services tournent convenablement et que nos rôles n’ont pas eu d’effet de bord les uns sur les autres.
Résultat de l’étude
Finalement, on obtient une série de tests qui, s’ils sont automatisés, permettent de couvrir à la fois le code Terraform et les différentes parties du code Ansible. Si on devait automatiser les tests, on obtiendrait une pipeline ressemblant à ça :
Dans le cas de notre exemple, nous devons cependant nuancer nos résultats. En testant les différentes strates de code individuellement, nous avons la garantie que notre code est fonctionnelle… individuellement. Rien ne nous dit que Ansible pourra tourner sur l’infrastructure générée par Terraform.
Que penser de Kitchen-CI ?
Kitchen-CI est un outil puissant. Il permet de réaliser un grand nombre de tests sur divers environnements que ce soit du code de construction d’infrastructure (Terraform…) ou du code de gestion de configuration (Ansible).
Concernant la partie construction d’infrastructure, l’utilisation de Kitchen-CI est pratique afin de valider et d’éviter une régression sur une infrastructure qui marche. Lors de l’écriture des tests et du code Terraform, on retrouve une certaine redondance liée à la nature “descriptive” du HCL et cela donne l’impression d’écrire du code en double. Dans notre cas, nous avons dû décrire le CIDR à la fois lors de l’écriture du Vnet et lors de l’écriture du test associé à ce Vnet par exemple. De plus, en fonction du cloud provider, les ressources de tests mises à disposition par InSpec sont pour le moment plutôt limitées (vous trouverez la liste ici). Actuellement, je recommanderais l’utilisation de Kitchen-CI avec Terraform dans deux cas d’usage :
1 - Écriture de code de validation d’infrastructure
2 - Écriture de module logique (ex : Module de convention de nommage…)
Dans le cas de Ansible, Kitchen-CI a un rôle similaire à celui de Molecule. Il permet de mettre en place des tests sur :
- divers rôles individuellement
- un playbook qui exécute une suite de rôle pour vérifier s’il n’y a pas de création d’effet de bord indésirable sur l’application.
En résumé :
Les plus |
Forte compatibilité avec InSpec<br><br> Écriture de test en Ruby (très bonne lisibilité).<br><br> Forte modularité<br><br> Peu de dépendance à installer. |
Les moins |
Mise en place complexe<br><br> Sensible au temps de création de l’infrastructure des divers cloud provider. |