Dzień dobry przyjaciele. W oczekiwaniu na rozpoczęcie nowego przepływu w tempie
Używanie Pulumi i języków programowania ogólnego przeznaczenia do kodu infrastruktury (Infrastructure as Code) zapewnia wiele korzyści: dostępność umiejętności i wiedzy, eliminację szablonów w kodzie poprzez abstrakcję, narzędzia znane Twojemu zespołowi, takie jak IDE i linters. Wszystkie te narzędzia inżynierii oprogramowania nie tylko zwiększają naszą produktywność, ale także poprawiają jakość naszego kodu. Dlatego naturalne jest, że użycie języków programowania ogólnego przeznaczenia pozwala nam wprowadzić kolejną ważną praktykę tworzenia oprogramowania - Testowanie.
W tym artykule przyjrzymy się, jak Pulumi pomaga nam testować naszą infrastrukturę jako kod.
Po co testować infrastrukturę?
Zanim przejdziemy do szczegółów, warto zadać sobie pytanie: „Po co w ogóle testować infrastrukturę?” Powodów jest wiele, a oto niektóre z nich:
- Testowanie jednostkowe poszczególnych funkcji lub fragmentów logiki programu
- Weryfikuje pożądany stan infrastruktury pod kątem określonych ograniczeń.
- Wykrywanie typowych błędów, takich jak brak szyfrowania zasobnika pamięci czy niezabezpieczony, otwarty dostęp z Internetu do maszyn wirtualnych.
- Sprawdzanie realizacji udostępnienia infrastruktury.
- Przeprowadzanie testów wykonawczych logiki aplikacji działającej w „zaprogramowanej” infrastrukturze w celu sprawdzenia funkcjonalności po udostępnieniu.
- Jak widzimy, istnieje szeroka gama opcji testowania infrastruktury. Polumi posiada mechanizmy testowania w każdym punkcie tego spektrum. Zacznijmy i zobaczmy, jak to działa.
Testów jednostkowych
Programy Pulumi pisane są w językach programowania ogólnego przeznaczenia, takich jak JavaScript, Python, TypeScript czy Go. Dlatego dostępna jest dla nich pełna moc tych języków, w tym ich narzędzia i biblioteki, w tym frameworki testowe. Pulumi obsługuje wiele chmur, co oznacza, że można go używać do testowania od dowolnego dostawcy chmury.
(W tym artykule, mimo że jest wielojęzyczny i wielochmurowy, używamy JavaScript i Mocha i skupiamy się na AWS. Możesz użyć Pythona unittest
, Przejdź do platformy testowej lub dowolnej innej platformy testowej, którą lubisz. I oczywiście Pulumi świetnie współpracuje z Azure, Google Cloud, Kubernetes.)
Jak widzieliśmy, istnieje kilka powodów, dla których warto przetestować kod infrastruktury. Jednym z nich są konwencjonalne testy jednostkowe. Ponieważ Twój kod może zawierać funkcje - na przykład obliczające CIDR, dynamicznie obliczające nazwy, tagi itp. - prawdopodobnie będziesz chciał je przetestować. Przypomina to pisanie regularnych testów jednostkowych dla aplikacji w ulubionym języku programowania.
Aby było trochę bardziej skomplikowanie, możesz sprawdzić, w jaki sposób Twój program przydziela zasoby. Aby to zilustrować, wyobraźmy sobie, że musimy stworzyć prosty serwer EC2 i chcemy mieć pewność, że:
- Instancje mają tag
Name
. - Instancje nie powinny używać wbudowanego skryptu
userData
- musimy użyć AMI (obrazu). - Internet nie powinien mieć dostępu do protokołu SSH.
Ten przykład opiera się na
indeks.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;
To jest podstawowy program Pulumi: po prostu przydziela grupę bezpieczeństwa EC2 i instancję. Należy jednak zaznaczyć, że łamiemy tu wszystkie trzy zasady wymienione powyżej. Napiszmy testy!
Pisanie testów
Ogólna struktura naszych testów będzie wyglądać jak zwykłe testy 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, открытого в Интернет.
});
});
Teraz napiszmy nasz pierwszy test: upewnij się, że instancje mają tag Name
. Aby to sprawdzić, po prostu pobieramy 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 ma kilka funkcji, na które warto zwrócić uwagę:
- Ponieważ przed wdrożeniem sprawdzamy stan zasobu, nasze testy są zawsze uruchamiane w trybie „planowania” (lub „podglądu”). Zatem istnieje wiele właściwości, których wartości po prostu nie zostaną odzyskane lub nie zostaną zdefiniowane. Obejmuje to wszystkie właściwości wyjściowe obliczone przez dostawcę usług w chmurze. Jest to normalne w przypadku naszych testów – sprawdzamy jedynie dane wejściowe. Do tej kwestii wrócimy później, jeśli chodzi o testy integracyjne.
- Ponieważ wszystkie właściwości zasobów Pulumi są danymi wyjściowymi, a wiele z nich jest ocenianych asynchronicznie, aby uzyskać dostęp do wartości, musimy użyć metody Apply. Jest to bardzo podobne do obietnic i funkcji
then
. - Ponieważ używamy kilku właściwości, aby wyświetlić numer URN zasobu w komunikacie o błędzie, musimy użyć tej funkcji
pulumi.all
aby je połączyć. - Wreszcie, ponieważ wartości te są obliczane asynchronicznie, musimy skorzystać z wbudowanej funkcji asynchronicznego wywołania zwrotnego Mocha
done
lub zwrócenie obietnicy.
Kiedy już wszystko skonfigurujemy, będziemy mieli dostęp do danych wejściowych w postaci prostych wartości JavaScript. Nieruchomość tags
jest mapą (tablicą asocjacyjną), więc upewnimy się, że (1) nie jest fałszywa i (2) istnieje klucz do Name
. To bardzo proste i teraz możemy przetestować wszystko!
Teraz napiszmy nasz drugi czek. 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 trochę bardziej skomplikowane, ponieważ szukamy reguł logowania powiązanych z grupą bezpieczeństwa, których może być wiele, oraz zakresów CIDR w tych regułach, których również może być wiele. Ale udało nam się:
// 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();
}
});
});
To wszystko. Teraz przeprowadźmy testy!
Uruchamianie testów
W większości przypadków testy można uruchamiać w zwykły sposób, korzystając z wybranego środowiska testowego. Ale jest jedna cecha Pulumi, na którą warto zwrócić uwagę.
Zazwyczaj do uruchamiania programów Pulumi wykorzystywany jest interfejs CLI pulimi (interfejs wiersza poleceń), który konfiguruje środowisko wykonawcze języka, kontroluje uruchamianie silnika Pulumi, dzięki czemu można rejestrować i uwzględniać operacje na zasobach w planie itp. Jednakże jest jeden problem. Podczas pracy pod kontrolą środowiska testowego nie będzie komunikacji pomiędzy interfejsem CLI a silnikiem Pulumi.
Aby obejść ten problem, wystarczy podać następujące informacje:
- Nazwa projektu zawarta w zmiennej środowiskowej
PULUMI_NODEJS_PROJECT
(lub, bardziej ogólnie,PULUMI__PROJECT для других языков).
Nazwa stosu określona w zmiennej środowiskowejPULUMI_NODEJS_STACK
(lub, bardziej ogólnie,PULUMI__ STACK).
Twoje zmienne konfiguracyjne stosu. Można je uzyskać za pomocą zmiennej środowiskowejPULUMI_CONFIG
a ich format to mapa JSON z parami klucz/wartość.Program wyświetli ostrzeżenia wskazujące, że połączenie z CLI/silnikiem nie jest dostępne podczas wykonywania. Jest to ważne, ponieważ Twój program w rzeczywistości niczego nie będzie wdrażał i może Cię zaskoczyć, jeśli nie jest to zamierzeniem! Aby powiedzieć Pulumi, że właśnie tego potrzebujesz, możesz zainstalować
PULUMI_TEST_MODE
вtrue
.Wyobraź sobie, że musimy określić nazwę projektu w
my-ws
, nazwa stosudev
i regionu AWSus-west-2
. Wiersz poleceń do uruchamiania 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.js
Wykonanie tego zgodnie z oczekiwaniami pokaże nam, że mamy trzy nieudane testy!
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
Naprawmy 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 ], // 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;
A następnie ponownie uruchom testy:
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)
Wszystko poszło dobrze... Hurra! ✓✓✓
To tyle na dziś, ale o testach wdrożeniowych porozmawiamy w drugiej części tłumaczenia 😉
Źródło: www.habr.com