Dzień dobry, przyjaciele. W przeddzień rozpoczęcia nowej edycji kursu dzielimy się z wami nowym tłumaczeniem. Zaczynajmy.

Użycie Pulumi oraz języków programowania ogólnego przeznaczenia do infrastruktury jako kodu (Infrastructure as Code) oferuje wiele korzyści: umiejętności i wiedza, eliminacja boilerplate'u w kodzie przez abstrakcję, znajome narzędzia dla waszego zespołu, takie jak IDE i lintery. Wszystkie te narzędzia inżynierii oprogramowania nie tylko sprawiają, że jesteśmy bardziej produktywni, ale także poprawiają jakość kodu. Dlatego całkowicie naturalne jest to, że użycie języków programowania ogólnego przeznaczenia pozwala na wdrożenie jeszcze jednej ważnej praktyki w rozwoju oprogramowania — testowanie.
W tym artykule omówimy, jak Pulumi pomaga testować naszą «infrastrukturę jako kod».

Dlaczego testować infrastrukturę?
Zanim przejdziemy do szczegółów, warto zadać pytanie: „Dlaczego w ogóle testować infrastrukturę?” Istnieje wiele powodów, oto niektóre z nich:
- Modularne testowanie poszczególnych funkcji lub fragmentów logiki twojego programu.
- Sprawdzenie, czy pożądany stan infrastruktury odpowiada określonym wymaganiom.
- Wykrywanie powszechnych błędów, takich jak brak szyfrowania storage bucket lub niechroniony, otwarty dostęp z Internetu do maszyn wirtualnych.
- Sprawdzenie wykonania provisioning infrastruktury.
- Wykonywanie testów runtime logiki aplikacji uruchomionej w twojej «zaprogamowanej» infrastrukturze dla weryfikacji funkcjonalności po provisioning.
- Jak widzimy, istnieje szeroki zakres opcji testowania infrastruktury. W Polumi dostępne są mechanizmy do testowania na każdym etapie tego zakresu. Zaczynajmy i zobaczmy, jak to działa.
Modularne testowanie
Programy Pulumi są tworzone w językach programowania ogólnego przeznaczenia, takich jak JavaScript, Python, TypeScript lub Go. Dlatego dostępna jest pełna moc tych języków, w tym ich narzędzia i biblioteki, w tym frameworki testowe. Pulumi jest wielochmurowe, co oznacza, że można używać dowolnych dostawców chmurowych do testów.
(W tym artykule, mimo wielojęzyczności i wielochmurowości, używamy JavaScript i Mocha, skupiając się na AWS. Można również używać Pythona unittest, testowy framework Go lub dowolny inny ulubiony framework do testowania. Oczywiście Pulumi doskonale współpracuje z Azure, Google Cloud, Kubernetes.)
Jak widzieliśmy, istnieje kilka powodów, dla których może być konieczne testowanie kodu infrastruktury. Jednym z nich jest standardowe testowanie jednostkowe. Ponieważ twój kod może mieć funkcje — na przykład do obliczania CIDR, dynamicznego obliczania nazw, tagów itd. — prawdopodobnie będziesz chciał je przetestować. To to samo, co pisanie standardowych testów jednostkowych dla aplikacji w twoim ulubionym języku programowania.
Jeśli nieco skomplikujemy, możemy sprawdzić, jak twoje programy zarządzają zasobami. Dla ilustracji załóżmy, że musimy stworzyć prosty serwer EC2 i chcemy mieć pewność, że:
- Instancje mają tag
Nazwa. - Instancje nie powinny używać skryptu inline
userData— musimy użyć AMI (obrazu). - Nie powinno być SSH otwartego w Internecie.
Ten przykład jest oparty na :
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 ], // odniesienie do obiektu grupy powyżej
ami: "ami-c55673a0" // AMI dla us-east-2 (Ohio),
userData: userData // uruchom prosty serwer WWW
});
exports.group = group;
exports.server = server;
exports.publicIp = server.publicIp;
exports.publicHostName = server.publicDns;
To podstawowy program Pulumi: po prostu alokuje grupę bezpieczeństwa EC2 i instancję. Należy jednak zauważyć, że tutaj łamiemy wszystkie trzy zasady opisane powyżej. Czas na pisanie testów!
Pisanie testów
Ogólna struktura naszych testów będzie wyglądała jak standardowe testy Mocha:
ec2tests.js
test.js:
let assert = require("assert");
let mocha = require("mocha");
let pulumi = require("@pulumi/pulumi");
let infra = require("./index");
describe("Infrastruktura", function() {
let server = infra.server;
describe("#server", function() {
// TODO(sprawdzenie 1): Powinien być tag Name.
// TODO(sprawdzenie 2): Nie powinno być skryptu inline userData.
});
let group = infra.group;
describe("#group", function() {
// TODO(sprawdzenie 3): Nie powinno być SSH otwartego w Internecie.
});
}); Teraz napiszmy nasz pierwszy test: upewnijmy się, że instancje mają tag Nazwa. Aby to sprawdzić, po prostu uzyskujemy obiekt instancji EC2 i sprawdzamy odpowiednią właściwość 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();
}
});
});Wygląda jak zwykły test, ale z kilkoma szczegółami, które zasługują na uwagę:
- Ponieważ pytamy o stan zasobów przed wdrożeniem, nasze testy zawsze są wykonywane w trybie „plan” (lub „preview”). W ten sposób istnieje wiele właściwości, których wartości po prostu nie zostaną pobrane lub będą nieokreślone. Wliczają się w to wszystkie właściwości wyjściowe obliczane przez Twojego dostawcę chmury. Dla naszych testów to w porządku — sprawdzamy tylko dane wejściowe. Do tego zagadnienia jeszcze wrócimy, gdy przyjdzie czas na testy integracyjne.
- Ponieważ wszystkie właściwości zasobów Pulumi są „wyjściami”, a wiele z nich jest obliczanych asynchronicznie, musimy użyć metody apply, aby uzyskać dostęp do wartości. Jest to bardzo podobne do obietnic i funkcji asynchronicznych.
then. - Ponieważ używamy kilku właściwości, aby pokazać URN zasobu w komunikacie o błędzie, musimy użyć funkcji
pulumi.all, aby je połączyć. - W końcu, ponieważ te wartości są obliczane asynchronicznie, musimy skorzystać z wbudowanej asynchronicznej możliwości Mocha z wywołaniem zwrotnym
zrobionelub zwracaniem obietnicy.
Po skonfigurowaniu wszystkiego, uzyskamy dostęp do danych wejściowych jako prostych wartości JavaScript. Właściwość tags to map (tablica asocjacyjna), więc po prostu upewnimy się, że jest (1) nie równoważne false i (2) istnieje klucz dla Nazwa. To bardzo proste i teraz możemy przetestować cokolwiek!
Teraz napiszmy naszą drugą kontrolę. To jeszcze prostsze:
// 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();
}
});
});I na koniec napiszmy trzeci test. Będzie to nieco bardziej skomplikowane, ponieważ szukamy reguł wejścia związanych z grupą zabezpieczeń, których może być wiele, oraz zakresów CIDR w tych regułach, których również może być dużo. Ale daliśmy radę:
// 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();
}
});
});I to wszystko. Teraz uruchommy testy!
Uruchamianie testów
Testy można uruchamiać w większości przypadków w zwykły sposób, używając wybranego przez Ciebie frameworka testowego. Ale jest jedna cecha Pulumi, na którą warto zwrócić uwagę.
Zazwyczaj do uruchamiania programów Pulumi używa się interfejsu wiersza poleceń pulimi CLI, który konfiguruje runtime języka, kontroluje uruchamianie silnika Pulumi, aby można było rejestrować operacje z zasobami i włączać je do planu, itd. Jednak jest jeden problem. Podczas uruchamiania pod kontrolą twojego frameworka testowego nie będzie połączenia między CLI a silnikiem Pulumi.
Aby obejść ten problem, musimy po prostu wskazać:
- Nazwę projektu, która znajduje się w zmiennej środowiskowej
PULUMI_NODEJS_PROJECT(lub, w bardziej ogólnym przypadku,PULUMI__PROJECT dla innych języków).
Nazwę stosu, która jest określona w zmiennej środowiskowejPULUMI_NODEJS_STACK(lub, w bardziej ogólnym przypadku,PULUMI__STACK).
Twoje zmienne konfiguracyjne stosu. Można je uzyskać za pomocą zmiennej środowiskowejPULUMI_CONFIGa ich format stanowi mapa JSON z parami klucz/wartość.Program wyświetli ostrzeżenia mówiące o tym, że podczas wykonywania nie ma połączenia z CLI/silnikiem. Jest to ważne, ponieważ tak naprawdę twoja aplikacja nic nie wdroży i może to być zaskoczeniem, jeśli nie o to ci chodziło! Aby powiedzieć Pulumi, że to właśnie jest to, czego potrzebujesz, możesz ustawić
PULUMI_TEST_MODEdotrue.Wyobraź sobie, że musimy określić nazwę projektu w
my-ws, nazwę stosudev, i region AWSus-west-2. Wiersz poleceń do uruchomienia testów Mocha będzie wyglądać następująco:$ PULUMI_TEST_MODE=true PULUMI_NODEJS_STACK="my-ws" PULUMI_NODEJS_PROJECT="dev" PULUMI_CONFIG='{ "aws:region": "us-west-2" }' mocha tests.jsWykonanie tego, jak się spodziewano, pokaże nam, że mamy trzy nieudane testy!
Infrastruktura #serwer 1) musi mieć etykietę nazwy 2) nie może używać userData (użyj zamiast tego AMI) #grupa 3) nie może otworzyć portu 22 (SSH) dla Internetu 0 zaliczonych (17ms) 3 niepowodzeń 1) Infrastruktura #serwer musi mieć etykietę nazwy: Błąd: Brak etykiety nazwy na serwerze urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www 2) Infrastruktura #serwer nie może używać userData (użyj zamiast tego AMI): Błąd: Nielegalne użycie userData na serwerze urn:pulumi:my-ws::my-dev::aws:ec2/instance:Instance::web-server-www 3) Infrastruktura #grupa nie może otworzyć portu 22 (SSH) dla Internetu: Błąd: Nielegalny port SSH 22 otwarty dla Internetu (CIDR 0.0.0.0/0) na grupieNaprawmy nasz program:
"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 ], // odwołanie do obiektu grupy powyżej ami: "ami-c55673a0" // AMI dla us-east-2 (Ohio), }); exports.group = group; exports.server = server; exports.publicIp = server.publicIp; exports.publicHostName = server.publicDns;A następnie ponownie uruchomimy testy:
Infrastruktura #serwer ✓ musi mieć etykietę z nazwą ✓ nie może używać userData (zamiast tego użyj AMI) #grupa ✓ nie może otwierać portu 22 (SSH) na Internet 3 zaliczone (16ms)Wszystko poszło pomyślnie… Hurra! ✓✓✓
Na dzisiaj to wszystko, o testowaniu wdrożenia porozmawiamy w drugiej części tłumaczenia 😉
Źródło: habr.com
