Infrastruktur als Code mit Pulumi testen. Teil 1

Guten Tag, Freunde. Im Vorfeld des Starts des neuen Kurses „DevOps-Praktiken und -Werkzeuge“ teilen wir mit euch die neue Übersetzung. Los geht's.

Infrastruktur als Code mit Pulumi testen. Teil 1

Die Verwendung von Pulumi und Programmiersprachen allgemeiner Bestimmung für Infrastruktur-Code (Infrastructure as Code) bietet viele Vorteile: die Verfügbarkeit von Fähigkeiten und Wissen, die Beseitigung von Boilerplate-Code durch Abstraktion, vertraute Werkzeuge für euer Team wie IDEs und Linter. All diese Software-Engineering-Werkzeuge machen uns nicht nur produktiver, sondern verbessern auch die Code-Qualität. Daher ist es nur natürlich, dass die Verwendung von Programmiersprachen allgemeiner Bestimmung eine weitere wichtige Praxis in der Softwareentwicklung ermöglicht — Testen.

In diesem Artikel werden wir erörtern, wie Pulumi uns dabei hilft, unsere "Infrastruktur als Code" zu testen.

Infrastruktur als Code mit Pulumi testen. Teil 1

Warum die Infrastruktur testen?

Bevor wir in die Einzelheiten eintauchen, sollte die Frage gestellt werden: „Warum überhaupt die Infrastruktur testen?“ Da gibt es viele Gründe, und hier sind einige davon:

  • Modultests von einzelnen Funktionen oder Logikfragmenten eures Programms.
  • Prüfung des gewünschten Zustands der Infrastruktur auf Übereinstimmung mit bestimmten Einschränkungen.
  • Erkennung häufiger Fehler, wie das Fehlen von Verschlüsselung für Storage-Buckets oder ungeschützten, offenen Zugriff aus dem Internet auf virtuelle Maschinen.
  • Überprüfung der Durchführung der Infrastruktur-Provisionierung.
  • Durchführung von Laufzeit-Tests der Logik einer Anwendung, die innerhalb eurer "programmierten" Infrastruktur läuft, um die Funktionsfähigkeit nach der Provisionierung zu überprüfen.
  • Wie wir sehen, gibt es ein breites Spektrum an Testmöglichkeiten für die Infrastruktur. In Pulumi gibt es Mechanismen, um an jedem Punkt dieses Spektrums zu testen. Lassen Sie uns anfangen und sehen, wie das funktioniert.

Modultests

Pulumi-Programme werden in Programmiersprachen allgemeiner Bestimmung geschrieben, wie JavaScript, Python, TypeScript oder Go. Daher steht ihnen die volle Kraft dieser Sprachen zur Verfügung, einschließlich ihrer Werkzeuge und Bibliotheken, einschließlich Test-Frameworks. Pulumi ist multicloud, was bedeutet, dass für Tests beliebige Cloud-Anbieter verwendet werden können.

(In diesem Artikel verwenden wir trotz Mehrsprachigkeit und Multicloud JavaScript und Mocha und orientieren uns an AWS. Python kann genutzt werden. unittest, das Test-Framework Go oder jedes andere von Ihnen bevorzugte Test-Framework. Und natürlich funktioniert Pulumi hervorragend mit Azure, Google Cloud, Kubernetes.)

Wie wir gesehen haben, gibt es mehrere Gründe, warum Sie Ihren Infrastrukturcode testen müssen. Einer davon ist das übliche Modultesten. Da Ihr Code Funktionen haben kann – beispielsweise zur Berechnung von CIDR, dynamischen Berechnungen von Namen, Tags usw. – möchten Sie diese wahrscheinlich testen. Das ist dasselbe wie das Schreiben standardmäßiger Modultests für Anwendungen in Ihrer bevorzugten Programmiersprache.
Wenn wir es etwas komplizierter machen, können wir prüfen, wie Ihr Programm Ressourcen verteilt. Zur Veranschaulichung stellen wir uns vor, dass wir einen einfachen EC2-Server erstellen müssen und sicherstellen wollen, dass Folgendes gegeben ist:

  • Die Instanzen haben einen Tag Name.
  • Instanzen sollten kein Inline-Skript verwenden userData — wir müssen AMI (Image) verwenden.
  • Es sollte kein SSH im Internet offen sein.

Dieses Beispiel basiert auf meinem Beispiel 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 ], // Referenz auf das oben genannte Gruppenobjekt
    ami: "ami-c55673a0"             // AMI für us-east-2 (Ohio),
    userData: userData              // Starte einen einfachen Webserver
});
 
exports.group = group;
exports.server = server;
exports.publicIp = server.publicIp;
exports.publicHostName = server.publicDns;

Dies ist das grundlegende Pulumi-Programm: Es allokiert einfach eine EC2-Sicherheitsgruppe und eine Instanz. Es ist jedoch zu beachten, dass wir hier alle drei oben genannten Regeln verletzen. Lassen Sie uns Tests schreiben!

Tests schreiben

Die allgemeine Struktur unserer Tests wird wie bei normalen Mocha-Tests aussehen:

ec2tests.js

test.js:
let assert = require("assert");
let mocha = require("mocha");
let pulumi = require("@pulumi/pulumi");
let infra = require("./index");
 
describe("Infrastruktur", function() {
    let server = infra.server;
    describe("#server", function() {
        // TODO(check 1): Es sollte ein Namens-Tag vorhanden sein.
        // TODO(check 2): Es sollte kein Inline-Skript userData geben.
    });
    let group = infra.group;
    describe("#group", function() {
        // TODO(check 3): Es sollte kein SSH im Internet offen sein.
    });
});

Lassen Sie uns jetzt unseren ersten Test schreiben: Stellen wir sicher, dass die Instanzen einen Tag haben Name. Um dies zu überprüfen, holen wir einfach das EC2-Instanzobjekt und überprüfen die entsprechende Eigenschaft. 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();
                }
            });
        });

Es sieht aus wie ein gewöhnlicher Test, hat jedoch einige Merkmale, die der Aufmerksamkeit bedürfen:

  • Da wir den Status der Ressource vor der Bereitstellung abfragen, werden unsere Tests immer im „Plan“-Modus (oder „Vorschau“) ausgeführt. Dadurch gibt es viele Eigenschaften, deren Werte einfach nicht erhalten werden oder undefiniert sind. Dazu gehören alle Ausgabeeigenschaften, die von Ihrem Cloud-Anbieter berechnet werden. Für unsere Tests ist das in Ordnung – wir überprüfen nur die Eingabewerte. Auf dieses Thema werden wir später zurückkommen, wenn wir zu den Integrationstests kommen.
  • Da alle Eigenschaften von Pulumi-Ressourcen „Ausgaben“ sind und viele von ihnen asynchron berechnet werden, müssen wir die Methode apply verwenden, um auf die Werte zuzugreifen. Das ist sehr ähnlich wie bei Promises und der async-Funktion. then .
  • Da wir mehrere Eigenschaften verwenden, um die URN der Ressource in der Fehlermeldung anzuzeigen, müssen wir die Funktion pulumi.all, um sie zu kombinieren.
  • Schließlich, da diese Werte asynchron berechnet werden, müssen wir die eingebaute asynchrone Fähigkeit von Mocha mit einem Callback fertig oder der Rückgabe eines Promises nutzen.

Nachdem wir alles eingerichtet haben, haben wir Zugriff auf die Eingabewerte wie auf einfache JavaScript-Werte. Die Eigenschaft tags ist ein Map (assoziatives Array), also stellen wir einfach sicher, dass es (1) nicht false ist und (2) einen Schlüssel für Name. Das ist sehr einfach und jetzt können wir alles überprüfen, was wir wollen!

Lassen Sie uns nun unsere zweite Überprüfung schreiben. Das ist noch einfacher:

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

Und schließlich schreiben wir einen dritten Test. Dieser wird etwas komplizierter, da wir nach Eingangsregeln suchen, die mit der Sicherheitsgruppe verbunden sind, von denen es viele geben kann, sowie nach CIDR-Bereichen in diesen Regeln, von denen es ebenfalls viele geben kann. Aber wir haben es geschafft:

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

Das ist alles. Lassen Sie uns jetzt die Tests ausführen!

Tests ausführen

Tests lassen sich in den meisten Fällen ganz normal ausführen, indem Sie das von Ihnen gewählte Test-Framework verwenden. Aber es gibt eine Besonderheit bei Pulumi, auf die man achten sollte.
Normalerweise wird zum Starten von Pulumi-Programmen die Pulumi-CLI (Command Line Interface, Befehlszeilenschnittstelle) verwendet, die die Laufzeitumgebung der Sprache einrichtet, die Ausführungen der Pulumi-Engine steuert, um Ressourcenvorgänge aufzuzeichnen und sie in den Plan einzufügen usw. Es gibt jedoch ein Problem. Wenn Sie unter Kontrolle Ihres Test-Frameworks starten, besteht keine Verbindung zwischen der CLI und der Pulumi-Engine.

Um dieses Problem zu umgehen, müssen wir einfach Folgendes angeben:

  • Der Projektname, der in der Umgebungsvariable PULUMI_NODEJS_PROJECT (oder im allgemeineren Fall, PULUMI__PROJECT für andere Sprachen).
    Der Stack-Name, der in der Umgebungsvariable PULUMI_NODEJS_STACK (oder im allgemeineren Fall, PULUMI__STACK).
    Ihre Stack-Konfigurationsvariablen. Sie können mit der Umgebungsvariable PULUMI_CONFIG abgerufen werden und ihr Format stellt eine JSON-Karte mit Schlüssel-Wert-Paaren dar.

    Das Programm gibt Warnungen aus, dass während der Ausführung keine Verbindung zur CLI/Engine verfügbar ist. Das ist wichtig, denn tatsächlich wird Ihr Programm nichts bereitstellen, und das könnte eine Überraschung werden, wenn das nicht das war, was Sie tun wollten! Um Pulumi mitzuteilen, dass dies genau das ist, was Sie benötigen, können Sie PULUMI_TEST_MODE in true.

    festlegen. Stellen Sie sich vor, wir müssen den Projektnamen angeben in my-ws, den Stacknamen dev, und die AWS-Region us-west-2. Die Befehlszeile zur Ausführung von Mocha-Tests würde wie folgt aussehen:

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

    Die Ausführung dessen wird, wie erwartet, uns zeigen, dass wir drei fehlgeschlagene Tests haben!

    Infrastructure
        #server
          1) muss ein Namens-Tag haben
     	 2) darf userData nicht verwenden (verwenden Sie stattdessen eine AMI)
        #group
          3) darf Port 22 (SSH) nicht für das Internet öffnen
    
      0 bestanden (17ms)
      3 fehlgeschlagen
     
     1) Infrastructure
           #server
             muss ein Namens-Tag haben:
         Fehler: Fehlendes Namens-Tag auf dem Server
            urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www
    
     2) Infrastructure
           #server
             darf userData nicht verwenden (verwenden Sie stattdessen eine AMI):
         Fehler: Illegale Verwendung von userData auf dem Server
            urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www
    
     3) Infrastructure
           #group
             darf Port 22 (SSH) nicht für das Internet öffnen:
         Fehler: Illegale Öffnung von SSH-Port 22 für das Internet (CIDR 0.0.0.0/0) auf der Gruppe

    Lassen Sie uns unser Programm korrigieren:

    "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 ], // verweist auf das oben genannte Gruppenobjekt
        ami: "ami-c55673a0"             // AMI für us-east-2 (Ohio),
    });
     
    exports.group = group;
    exports.server = server;
    exports.publicIp = server.publicIp;
    exports.publicHostName = server.publicDns;
    

    Und dann führen wir die Tests erneut aus:

    Infrastruktur
        #server
          ✓ muss ein Namensschild haben
          ✓ darf keine Benutzerdaten verwenden (verwenden Sie stattdessen ein AMI)
        #gruppe
          ✓ darf Port 22 (SSH) nicht für das Internet öffnen
     
     
     3 erfolgreich (16ms)

    Alles verlief erfolgreich… Hurra! ✓✓✓

    Das ist alles für heute, und über das Testen der Bereitstellung sprechen wir im zweiten Teil der Übersetzung 😉

Quelle: habr.com

60GB SSD 8Gb DDR4