Testing dell'infrastruttura come codice con Pulumi. Parte 1

Buongiorno, amici. In vista dell'inizio di un nuovo ciclo del corso «Pratiche e strumenti DevOps» condividiamo con voi una nuova traduzione. Iniziamo.

Testing dell'infrastruttura come codice con Pulumi. Parte 1

L'utilizzo di Pulumi e di linguaggi di programmazione di uso generale per il codice infrastrutturale (Infrastructure as Code) offre numerosi vantaggi: le competenze e le conoscenze necessarie, l'eliminazione del boilerplate attraverso l'astrazione, strumenti familiari al vostro team come IDE e linters. Tutti questi strumenti di ingegneria del software non solo ci rendono più produttivi, ma migliorano anche la qualità del codice. Pertanto, è del tutto naturale che l'uso di linguaggi di programmazione di uso generale consenta di implementare un'altra pratica importante nello sviluppo software — un test.

In questo articolo esamineremo come Pulumi ci aiuta a testare la nostra «infrastruttura come codice».

Testing dell'infrastruttura come codice con Pulumi. Parte 1

Perché testare l'infrastruttura?

Prima di addentrarci nei dettagli, è importante porre la domanda: «Perché testare l'infrastruttura?» Ci sono molti motivi e ecco alcuni di essi:

  • Testare in modo modulare singole funzioni o frammenti della logica del vostro programma
  • Verifica che lo stato desiderato dell'infrastruttura rispetti specifiche limitazioni.
  • Rilevamento di errori comuni, come l'assenza di crittografia per il bucket di archiviazione o accesso non protetto e aperto a Internet per le macchine virtuali.
  • Verifica dell'esecuzione del provisioning dell'infrastruttura.
  • Esecuzione di test runtime della logica dell'applicazione in esecuzione all'interno della tua infrastruttura 'programmata' per verificarne il funzionamento post-provisioning.
  • Come possiamo vedere, ci sono molte opzioni per testare l'infrastruttura. In Polumi ci sono meccanismi per testare in ogni punto di questo spettro. Cominciamo e vediamo come funziona.

Test unitari

I programmi Pulumi sono creati in linguaggi di programmazione generali, come JavaScript, Python, TypeScript o Go. Pertanto, hanno accesso a tutta la potenza di questi linguaggi, compresi i loro strumenti e librerie, inclusi i framework di test. Pulumi è multi-cloud, il che significa che è possibile utilizzare per i test qualsiasi fornitore di servizi cloud.

(In questo articolo, nonostante la multilinguismo e il multi-cloud, utilizziamo JavaScript e Mocha e ci orientiamo verso AWS. È possibile utilizzare Python unittest, un framework di test Go o qualsiasi altro framework di test che preferite. E, naturalmente, Pulumi funziona ottimamente con Azure, Google Cloud, Kubernetes.)

Come abbiamo visto, ci sono diverse ragioni per cui potrebbe essere necessario testare il vostro codice infrastrutturale. Una di queste è il comune test unitario. Poiché il vostro codice può avere funzioni – ad esempio, per calcolare CIDR, calcolare dinamicamente nomi, tag, ecc. – probabilmente vorrete testarle. È lo stesso che scrivere normali test unitari per applicazioni nel vostro linguaggio di programmazione preferito.
Se vogliamo complicare un po' le cose, possiamo controllare come il vostro programma distribuisce le risorse. Per illustrare, immaginiamo di dover creare un semplice server EC2 e vogliamo assicurarci che:

  • Le istanze abbiano un tag Name.
  • Le istanze non devono usare script inline userData – dobbiamo usare AMI (immagine).
  • Non deve esserci SSH aperto su Internet.

Questo esempio è stato scritto ispirandosi a esempio 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;

Questo è un programma Pulumi di base: allocazione di un gruppo di sicurezza EC2 e di un'istanza. Tuttavia, è importante notare che qui violiamo tutte e tre le regole sopra elencate. Iniziamo a scrivere i test!

Scriviamo test

La struttura generale dei nostri test sarà la stessa degli standard test Mocha:

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): Deve esserci un tag Name.
        // TODO(check 2): Non deve esserci uno script inline userData.
    });
    let group = infra.group;
    describe("#group", function() {
        // TODO(check 3): SSH non dovrebbe essere aperto su Internet.
    });
});

Ora scriviamo il nostro primo test: assicuriamoci che le istanze abbiano un tag Name. Per verificare questo, otteniamo semplicemente l'oggetto dell'istanza EC2 e controlliamo la proprietà corrispondente 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();
                }
            });
        });

Sembra un test normale, ma presenta alcune caratteristiche degne di nota:

  • Poiché stiamo richiedendo lo stato della risorsa prima del deployment, i nostri test vengono sempre eseguiti in modalità “plan” (o “preview”). Pertanto, ci sono molte proprietà i cui valori semplicemente non verranno restituiti o saranno indefiniti. Questo include tutte le proprietà di output calcolate dal tuo fornitore di cloud. Per i nostri test va bene — verifichiamo solo gli input. Torneremo su questo argomento più avanti, quando parleremo dei test di integrazione.
  • Poiché tutte le proprietà delle risorse Pulumi sono "output" e molte di esse vengono calcolate in modo asincrono, dobbiamo utilizzare il metodo apply per accedere ai valori. Questo è molto simile alle promesse e alle funzioni asincrone then .
  • Poiché utilizziamo diverse proprietà per mostrare l'URN della risorsa nel messaggio di errore, dobbiamo utilizzare la funzione pulumi.all, per unirli.
  • Infine, poiché questi valori vengono calcolati in modo asincrono, dobbiamo utilizzare la funzionalità asincrona integrata di Mocha con una callback fatto o restituendo una promessa.

Dopo aver configurato tutto, avremo accesso ai dati di input come semplici valori JavaScript. La proprietà tags è una mappa (array associativo), quindi ci assicureremo solo che (1) non sia false, e (2) ci sia una chiave per Name. È molto semplice e ora possiamo verificare qualsiasi cosa!

Ora scriviamo il nostro secondo test. È ancora più semplice:

 // 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();
                }
            });
        });

E, infine, scriviamo il terzo test. Sarà un po' più complesso, perché stiamo cercando regole di ingresso associate ai gruppi di sicurezza, che possono essere numerosi, e agli intervalli CIDR in queste regole, che possono anche essere molti. Ma ce l'abbiamo fatta:

    // 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();
                }
            });
        });

Ecco fatto. Ora eseguiamo i test!

Esecuzione dei test

Eseguire i test, nella maggior parte dei casi, può essere fatto nel modo consueto, utilizzando il framework di test scelto. Ma c'è una particolarità di Pulumi a cui vale la pena prestare attenzione.
Di solito, per avviare i programmi Pulumi, si utilizza il pulumi CLI (Command Line Interface), che configura il runtime del linguaggio, gestisce i lanci del motore Pulumi per registrare le operazioni sulle risorse e per includerle nel piano, e così via. Tuttavia, c'è un problema. Quando viene eseguito sotto il controllo del tuo framework di test, non ci sarà connessione tra il CLI e il motore Pulumi.

Per aggirare questo problema, è sufficiente specificare quanto segue:

  • Il nome del progetto, contenuto nella variabile d'ambiente PULUMI_NODEJS_PROJECT (o, in un caso più generale, PULUMI__PROJECT per altri linguaggi).
    Il nome dello stack, che è specificato nella variabile d'ambiente PULUMI_NODEJS_STACK (o, in un caso più generale, PULUMI__STACK).
    Le tue variabili di configurazione dello stack. Possono essere ottenute tramite la variabile d'ambiente PULUMI_CONFIG e il loro formato è una mappa JSON con coppie chiave/valore.

    Il programma genererà avvisi che indicano che non c'è connessione con CLI/engine durante l'esecuzione. Questo è importante perché in realtà, il vostro programma non sta distribuendo nulla e può essere una sorpresa se non è ciò che volevate fare! Per indicare a Pulumi che è proprio questo che desiderate, potete impostare PULUMI_TEST_MODE in true.

    Immaginate di dover specificare il nome del progetto in my-ws, il nome dello stack dev, e la regione AWS us-west-2. La riga di comando per eseguire i test Mocha apparirà come segue:

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

    Eseguire questo, come previsto, ci mostrerà che abbiamo tre test falliti!

    Infrastructure
        #server
          1) deve avere un tag nome
     	 2) non deve usare userData (usare un AMI invece)
        #group
          3) non deve aprire la porta 22 (SSH) su Internet
    
      0 passaggi (17ms)
      3 fallimenti
     
     1) Infrastructure
           #server
             deve avere un tag nome:
         Errore: Tag nome mancante sul server
            urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www
    
     2) Infrastructure
           #server
             non deve usare userData (usare un AMI invece):
         Errore: Uso illegale di userData sul server
            urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www
    
     3) Infrastructure
           #group
             non deve aprire la porta 22 (SSH) su Internet:
         Errore: Porta SSH 22 aperta illegalmente su Internet (CIDR 0.0.0.0/0) sul gruppo

    Correggiamo il nostro programma:

    "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;
    

    E poi riavvieremo i test:

    Infrastruttura
        #server
          ✓ deve avere un tag nome
          ✓ non deve utilizzare userData (utilizza un AMI invece)
        #group
          ✓ non deve aprire la porta 22 (SSH) a Internet
     
     
     3 superati (16ms)

    Tutto è andato a buon fine… Evviva! ✓✓✓

    Per oggi è tutto, mentre parleremo del test di deployment nella seconda parte della traduzione 😉

Fonte: habr.com

Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS 🔥 Acquista hosting affidabile per siti web con protezione DDoS, server VPS VDS | ProHoster