Tester l'infrastructure en tant que code avec Pulumi. Partie 1

Bonjour les amis. En prévision du démarrage d'un nouveau flux au rythme "Pratiques et outils DevOps" Nous partageons avec vous une nouvelle traduction. Aller.

Tester l'infrastructure en tant que code avec Pulumi. Partie 1

L'utilisation de Pulumi et des langages de programmation à usage général pour le code d'infrastructure (Infrastructure as Code) offre de nombreux avantages : la disponibilité de compétences et de connaissances, l'élimination du passe-partout dans le code grâce à l'abstraction, des outils familiers à votre équipe, tels que les IDE et les linters. Tous ces outils de génie logiciel nous rendent non seulement plus productifs, mais améliorent également la qualité de notre code. Par conséquent, il est tout à fait naturel que l'utilisation de langages de programmation à usage général nous permette d'introduire une autre pratique importante en matière de développement logiciel : test.

Dans cet article, nous verrons comment Pulumi nous aide à tester notre infrastructure en tant que code.

Tester l'infrastructure en tant que code avec Pulumi. Partie 1

Pourquoi tester l’infrastructure ?

Avant d’entrer dans les détails, il convient de se poser la question : « Pourquoi tester l’infrastructure ? » Il y a plusieurs raisons à cela et en voici quelques-unes :

  • Tests unitaires de fonctions individuelles ou de fragments de la logique de votre programme
  • Vérifie l’état souhaité de l’infrastructure par rapport à certaines contraintes.
  • Détection d'erreurs courantes, telles que l'absence de chiffrement d'un compartiment de stockage ou un accès ouvert et non protégé depuis Internet aux machines virtuelles.
  • Vérifier la mise en œuvre de l'approvisionnement de l'infrastructure.
  • Effectuer des tests d'exécution de la logique d'application exécutée dans votre infrastructure « programmée » pour vérifier la fonctionnalité après le provisionnement.
  • Comme nous pouvons le constater, il existe un large éventail d’options de test d’infrastructure. Polumi dispose de mécanismes pour tester à chaque point de ce spectre. Commençons et voyons comment cela fonctionne.

Tests unitaires

Les programmes Pulumi sont écrits dans des langages de programmation généraux tels que JavaScript, Python, TypeScript ou Go. Par conséquent, toute la puissance de ces langages, y compris leurs outils et bibliothèques, y compris les frameworks de test, est à leur disposition. Pulumi est multi-cloud, ce qui signifie qu'il peut être utilisé pour des tests auprès de n'importe quel fournisseur de cloud.

(Dans cet article, bien que multilingues et multicloud, nous utilisons JavaScript et Mocha et nous concentrons sur AWS. Vous pouvez utiliser Python unittest, Allez au framework de test, ou à tout autre framework de test que vous aimez. Et bien sûr, Pulumi fonctionne très bien avec Azure, Google Cloud, Kubernetes.)

Comme nous l'avons vu, il existe plusieurs raisons pour lesquelles vous souhaiterez peut-être tester votre code d'infrastructure. L’un d’eux est le test unitaire conventionnel. Parce que votre code peut avoir des fonctions - par exemple, calculer le CIDR, calculer dynamiquement des noms, des balises, etc. - vous voudrez probablement les tester. Cela revient à écrire des tests unitaires réguliers pour des applications dans votre langage de programmation préféré.
Pour être un peu plus compliqué, vous pouvez vérifier comment votre programme alloue les ressources. Pour illustrer, imaginons que nous devions créer un simple serveur EC2 et que nous voulions être sûrs des éléments suivants :

  • Les instances ont une balise Name.
  • Les instances ne doivent pas utiliser de script en ligne userData - il faut utiliser une AMI (image).
  • Aucun SSH ne devrait être exposé à Internet.

Cet exemple est basé sur mon exemple aws-js-webserver:

index.js :

"use strict";
 
let aws = require("@pulumi/aws");
 
let group = new aws.ec2.SecurityGroup("web-secgrp", {
    ingress: [
        { protocol: "tcp", fromPort: 22, toPort: 22, cidrBlocks: ["0.0.0.0/0"] },
        { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
    ],
});
 
let userData =
`#!/bin/bash
echo "Hello, World!" > index.html
nohup python -m SimpleHTTPServer 80 &`;
 
let server = new aws.ec2.Instance("web-server-www", {
    instanceType: "t2.micro",
    securityGroups: [ group.name ], // reference the group object above
    ami: "ami-c55673a0"             // AMI for us-east-2 (Ohio),
    userData: userData              // start a simple web server
});
 
exports.group = group;
exports.server = server;
exports.publicIp = server.publicIp;
exports.publicHostName = server.publicDns;

Il s'agit du programme de base de Pulumi : il alloue simplement un groupe de sécurité EC2 et une instance. Cependant, il convient de noter que nous enfreignons ici les trois règles énoncées ci-dessus. Écrivons des tests !

Écrire des tests

La structure générale de nos tests ressemblera à celle des tests Mocha classiques :

ec2tests.js

test.js:
let assert = require("assert");
let mocha = require("mocha");
let pulumi = require("@pulumi/pulumi");
let infra = require("./index");
 
describe("Infrastructure", function() {
    let server = infra.server;
    describe("#server", function() {
        // TODO(check 1): Должен быть тэг Name.
        // TODO(check 2): Не должно быть inline-скрипта userData.
    });
    let group = infra.group;
    describe("#group", function() {
        // TODO(check 3): Не должно быть SSH, открытого в Интернет.
    });
});

Écrivons maintenant notre premier test : assurez-vous que les instances ont la balise Name. Pour vérifier cela, nous obtenons simplement l'objet d'instance EC2 et vérifions la propriété correspondante tags:

 // check 1: Должен быть тэг Name.
        it("must have a name tag", function(done) {
            pulumi.all([server.urn, server.tags]).apply(([urn, tags]) => {
                if (!tags || !tags["Name"]) {
                    done(new Error(`Missing a name tag on server ${urn}`));
                } else {
                    done();
                }
            });
        });

Cela ressemble à un test classique, mais avec quelques fonctionnalités à noter :

  • Parce que nous interrogeons l’état d’une ressource avant le déploiement, nos tests sont toujours exécutés en mode « plan » (ou « aperçu »). Ainsi, il existe de nombreuses propriétés dont les valeurs ne seront tout simplement pas récupérées ou ne seront pas définies. Cela inclut toutes les propriétés de sortie calculées par votre fournisseur de cloud. C'est normal pour nos tests - nous vérifions uniquement les données d'entrée. Nous reviendrons sur cette problématique plus tard, lorsqu'il s'agira des tests d'intégration.
  • Étant donné que toutes les propriétés des ressources Pulumi sont des sorties et que beaucoup d'entre elles sont évaluées de manière asynchrone, nous devons utiliser la méthode apply pour accéder aux valeurs. Ceci est très similaire aux promesses et à la fonction then .
  • Puisque nous utilisons plusieurs propriétés pour afficher l'URN de la ressource dans le message d'erreur, nous devons utiliser la fonction pulumi.allpour les combiner.
  • Enfin, puisque ces valeurs sont calculées de manière asynchrone, nous devons utiliser la fonction de rappel asynchrone intégrée de Mocha. done ou retourner une promesse.

Une fois que nous aurons tout configuré, nous aurons accès aux entrées sous forme de simples valeurs JavaScript. Propriété tags est une carte (tableau associatif), nous allons donc simplement nous assurer que ce n'est pas (1) faux, et (2) il y a une clé pour Name. C'est très simple et maintenant on peut tout tester !

Maintenant, faisons notre deuxième chèque. C'est encore plus simple :

 // check 2: Не должно быть inline-скрипта userData.
        it("must not use userData (use an AMI instead)", function(done) {
            pulumi.all([server.urn, server.userData]).apply(([urn, userData]) => {
                if (userData) {
                    done(new Error(`Illegal use of userData on server ${urn}`));
                } else {
                    done();
                }
            });
        });

Et enfin, écrivons le troisième test. Ce sera un peu plus compliqué car nous recherchons les règles de connexion associées au groupe de sécurité, qui peuvent être nombreuses, et les plages CIDR dans ces règles, qui peuvent également être nombreuses. Mais nous avons réussi :

    // check 3: Не должно быть SSH, открытого в Интернет.
        it("must not open port 22 (SSH) to the Internet", function(done) {
            pulumi.all([ group.urn, group.ingress ]).apply(([ urn, ingress ]) => {
                if (ingress.find(rule =>
                        rule.fromPort == 22 && rule.cidrBlocks.find(block =>
                            block === "0.0.0.0/0"))) {
                    done(new Error(`Illegal SSH port 22 open to the Internet (CIDR 0.0.0.0/0) on group ${urn}`));
                } else {
                    done();
                }
            });
        });

C'est tout. Lançons maintenant les tests !

Exécution de tests

Dans la plupart des cas, vous pouvez exécuter des tests de la manière habituelle, en utilisant le framework de test de votre choix. Mais il y a une caractéristique de Pulumi à laquelle il convient de prêter attention.
Généralement, pour exécuter les programmes Pulumi, on utilise la CLI pulimi (interface de ligne de commande), qui configure le temps d'exécution du langage, contrôle le lancement du moteur Pulumi afin que les opérations avec les ressources puissent être enregistrées et incluses dans le plan, etc. Cependant, il y a un problème. Lors de l'exécution sous le contrôle de votre framework de test, il n'y aura aucune communication entre la CLI et le moteur Pulumi.

Pour contourner ce problème, il suffit de préciser les éléments suivants :

  • Nom du projet, contenu dans la variable d'environnement PULUMI_NODEJS_PROJECT (ou plus généralement PULUMI__PROJECT для других языков).
    Le nom de la pile spécifié dans la variable d'environnement PULUMI_NODEJS_STACK (ou plus généralement PULUMI__ STACK).
    Variables de configuration de votre pile. Ils peuvent être obtenus à l'aide d'une variable d'environnement PULUMI_CONFIG et leur format est une carte JSON avec des paires clé/valeur.

    Le programme émettra des avertissements indiquant que la connexion à la CLI/au moteur n'est pas disponible pendant l'exécution. Ceci est important car votre programme ne déploiera rien et cela peut surprendre si ce n'est pas ce que vous aviez l'intention de faire ! Pour dire à Pulumi que c'est exactement ce dont vous avez besoin, vous pouvez installer PULUMI_TEST_MODE в true.

    Imaginez que nous devions spécifier le nom du projet dans my-ws, nom de la pile devet région AWS us-west-2. La ligne de commande pour exécuter les tests Mocha ressemblera à ceci :

    $ PULUMI_TEST_MODE=true 
        PULUMI_NODEJS_STACK="my-ws" 
        PULUMI_NODEJS_PROJECT="dev" 
        PULUMI_CONFIG='{ "aws:region": "us-west-2" }' 
        mocha tests.js

    Faire cela, comme prévu, nous montrera que nous avons trois tests échoués !

    Infrastructure
        #server
          1) must have a name tag
     	 2) must not use userData (use an AMI instead)
        #group
          3) must not open port 22 (SSH) to the Internet
    
      0 passing (17ms)
      3 failing
     
     1) Infrastructure
           #server
             must have a name tag:
         Error: Missing a name tag on server
            urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www
    
     2) Infrastructure
           #server
             must not use userData (use an AMI instead):
         Error: Illegal use of userData on server
            urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www
    
     3) Infrastructure
           #group
             must not open port 22 (SSH) to the Internet:
         Error: Illegal SSH port 22 open to the Internet (CIDR 0.0.0.0/0) on group

    Corrigeons notre programme :

    "use strict";
     
    let aws = require("@pulumi/aws");
     
    let group = new aws.ec2.SecurityGroup("web-secgrp", {
        ingress: [
            { protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] },
        ],
    });
     
    let server = new aws.ec2.Instance("web-server-www", {
        tags: { "Name": "web-server-www" },
        instanceType: "t2.micro",
        securityGroups: [ group.name ], // reference the group object above
        ami: "ami-c55673a0"             // AMI for us-east-2 (Ohio),
    });
     
    exports.group = group;
    exports.server = server;
    exports.publicIp = server.publicIp;
    exports.publicHostName = server.publicDns;
    

    Et puis relancez les tests :

    Infrastructure
        #server
          ✓ must have a name tag
          ✓ must not use userData (use an AMI instead)
        #group
          ✓ must not open port 22 (SSH) to the Internet
     
     
     3 passing (16ms)

    Tout s'est bien passé... Hourra ! ✓ ✓ ✓

    C'est tout pour aujourd'hui, mais nous parlerons des tests de déploiement dans la deuxième partie de la traduction 😉

Source: habr.com

Ajouter un commentaire