Testare l'infrastruttura come codice con Pulumi. Parte 1

Buon pomeriggio amici. In previsione dell'inizio di un nuovo flusso al ritmo "Pratiche e strumenti DevOps" Condividiamo con voi una nuova traduzione. Andare.

Testare l'infrastruttura come codice con Pulumi. Parte 1

L'utilizzo di Pulumi e dei linguaggi di programmazione generici per il codice dell'infrastruttura (Infrastructure as Code) offre numerosi vantaggi: disponibilità di competenze e conoscenze, eliminazione del boilerplate nel codice attraverso l'astrazione, strumenti familiari al team, come IDE e linter. Tutti questi strumenti di ingegneria del software non solo ci rendono più produttivi, ma migliorano anche la qualità del nostro codice. Pertanto, è naturale che l'uso di linguaggi di programmazione generici ci permetta di introdurre un'altra importante pratica di sviluppo software: test.

In questo articolo vedremo come Pulumi ci aiuta a testare la nostra infrastruttura come codice.

Testare l'infrastruttura come codice con Pulumi. Parte 1

Perché testare l'infrastruttura?

Prima di entrare nei dettagli, vale la pena porsi la domanda: “Perché testare l’infrastruttura?” Ci sono molte ragioni per questo ed eccone alcune:

  • Test unitari di singole funzioni o frammenti della logica del programma
  • Verifica lo stato desiderato dell'infrastruttura rispetto a determinati vincoli.
  • Rilevamento di errori comuni, come la mancanza di crittografia di un bucket di archiviazione o l'accesso aperto e non protetto da Internet alle macchine virtuali.
  • Verifica dell’implementazione della fornitura dell’infrastruttura.
  • Esecuzione di test runtime della logica applicativa in esecuzione all'interno dell'infrastruttura “programmata” per verificarne la funzionalità dopo il provisioning.
  • Come possiamo vedere, esiste un’ampia gamma di opzioni di test dell’infrastruttura. Polumi dispone di meccanismi per testare ogni punto di questo spettro. Cominciamo e vediamo come funziona.

Test unitari

I programmi Pulumi sono scritti in linguaggi di programmazione generici come JavaScript, Python, TypeScript o Go. Pertanto, hanno a disposizione tutta la potenza di questi linguaggi, inclusi i loro strumenti e librerie, compresi i framework di test. Pulumi è multi-cloud, il che significa che può essere utilizzato per i test da qualsiasi provider cloud.

(In questo articolo, nonostante sia multilingue e multicloud, utilizziamo JavaScript e Mocha e ci concentriamo su AWS. Puoi utilizzare Python unittest, Vai al framework di test o qualsiasi altro framework di test che ti piace. E, naturalmente, Pulumi funziona alla grande con Azure, Google Cloud, Kubernetes.)

Come abbiamo visto, ci sono diversi motivi per cui potresti voler testare il codice della tua infrastruttura. Uno di questi è il test unitario convenzionale. Perché il tuo codice potrebbe avere funzioni, ad esempio per calcolare CIDR, calcolare dinamicamente nomi, tag, ecc. - probabilmente vorrai testarli. È come scrivere regolari test unitari per le applicazioni nel tuo linguaggio di programmazione preferito.
Per diventare un po' più complicato, puoi controllare come il tuo programma alloca le risorse. Per illustrare, immaginiamo di dover creare un semplice server EC2 e di voler essere sicuri di quanto segue:

  • Le istanze hanno un tag Name.
  • Le istanze non devono utilizzare script in linea userData - dobbiamo utilizzare un'AMI (immagine).
  • Non dovrebbe esserci alcun SSH esposto a Internet.

Questo esempio è basato su il mio esempio aws-js-webserver:

indice.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 è il programma Pulumi base: alloca semplicemente un gruppo di sicurezza EC2 e un'istanza. Tuttavia, va notato che qui stiamo infrangendo tutte e tre le regole sopra indicate. Scriviamo i test!

Prove di scrittura

La struttura generale dei nostri test sarà simile ai normali 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): Должен быть тэг Name.
        // TODO(check 2): Не должно быть inline-скрипта userData.
    });
    let group = infra.group;
    describe("#group", function() {
        // TODO(check 3): Не должно быть SSH, открытого в Интернет.
    });
});

Ora scriviamo il nostro primo test: assicuriamoci che le istanze abbiano il tag Name. Per verificarlo, prendiamo semplicemente l'oggetto 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 con alcune caratteristiche degne di nota:

  • Poiché interroghiamo lo stato di una risorsa prima della distribuzione, i nostri test vengono sempre eseguiti in modalità "piano" (o "anteprima"). Pertanto, ci sono molte proprietà i cui valori semplicemente non verranno recuperati o non verranno definiti. Ciò include tutte le proprietà di output calcolate dal tuo provider cloud. Questo è normale per i nostri test: controlliamo solo i dati di input. Torneremo su questo argomento più avanti, quando si tratterà dei test di integrazione.
  • Poiché tutte le proprietà delle risorse Pulumi sono output e molte di esse vengono valutate in modo asincrono, è necessario utilizzare il metodo apply per accedere ai valori. Questo è molto simile alle promesse e alle funzioni then .
  • Poiché stiamo utilizzando diverse proprietà per mostrare l'URN della risorsa nel messaggio di errore, dobbiamo utilizzare la funzione pulumi.allper combinarli.
  • Infine, poiché questi valori vengono calcolati in modo asincrono, dobbiamo utilizzare la funzionalità di callback asincrona incorporata di Mocha done o restituire una promessa.

Una volta impostato tutto, avremo accesso agli input come semplici valori JavaScript. Proprietà tags è una mappa (array associativo), quindi ci assicureremo semplicemente che sia (1) non falso e (2) ci sia una chiave per Name. È molto semplice e ora possiamo testare qualsiasi cosa!

Ora scriviamo il nostro secondo assegno. È 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. Questo sarà un po' più complicato perché stiamo cercando le regole di accesso associate al gruppo di sicurezza, che possono essercene molte, e gli intervalli CIDR in tali regole, che possono anche essercene molti. Ma siamo riusciti:

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

È tutto. Ora eseguiamo i test!

Esecuzione di test

Nella maggior parte dei casi, è possibile eseguire i test nel modo consueto, utilizzando il framework di test di propria scelta. Ma c'è una caratteristica di Pulumi a cui vale la pena prestare attenzione.
Tipicamente, per eseguire i programmi Pulumi, viene utilizzata la CLI (Command Line Interface) pulimi, che configura il runtime del linguaggio, controlla l'avvio del motore Pulumi in modo che le operazioni con le risorse possano essere registrate e incluse nel piano, ecc. Tuttavia, c’è un problema. Quando si esegue sotto il controllo del framework di test, non ci sarà comunicazione tra la CLI e il motore Pulumi.

Per aggirare questo problema, dobbiamo solo specificare quanto segue:

  • Nome del progetto, contenuto nella variabile di ambiente PULUMI_NODEJS_PROJECT (o, più in generale, PULUMI__PROJECT для других языков).
    Il nome dello stack specificato nella variabile di ambiente PULUMI_NODEJS_STACK (o, più in generale, PULUMI__ STACK).
    Le variabili di configurazione dello stack. Possono essere ottenuti utilizzando una variabile di ambiente PULUMI_CONFIG e il loro formato è una mappa JSON con coppie chiave/valore.

    Il programma emetterà avvisi indicanti che la connessione alla CLI/motore non è disponibile durante l'esecuzione. Questo è importante perché il tuo programma in realtà non distribuirà nulla e potrebbe essere una sorpresa se non è quello che intendevi fare! Per dire a Pulumi che questo è esattamente ciò di cui hai bisogno, puoi installare PULUMI_TEST_MODE в true.

    Immaginiamo di dover specificare il nome del progetto in my-ws, nome dello stack deve la regione AWS us-west-2. La riga di comando per eseguire i test Mocha sarà simile alla seguente:

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

    Facendo questo, come previsto, ci mostreremo che abbiamo tre test falliti!

    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

    Sistemiamo 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 esegui nuovamente i test:

    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)

    Tutto è andato bene... Evviva! ✓ ✓ ✓

    Per oggi è tutto, ma dei test di implementazione parleremo nella seconda parte della traduzione 😉

Fonte: habr.com

Aggiungi un commento