Добрий день друзі. Напередодні старту нового потоку за курсом
Використання Pulumi та мов програмування загального призначення для інфраструктурного коду (Infrastructure as Code) дає багато переваг: наявність навичок та знань, усунення в коді бойлерплейту через абстракцію, знайомі вашій команді інструменти, такі як IDE та лінтери. Всі ці інструменти програмної інженерії не тільки роблять нас більш продуктивними, але й покращують якість коду. Тому цілком природно, що використання мов програмування загального призначення дозволяє впровадити ще одну важливу практику розробки програмного забезпечення. тестування.
У цій статті ми розглянемо, як Pulumi допомагає тестувати нашу «інфраструктуру як код».
Навіщо випробувати інфраструктуру?
Перш ніж вдаватися до подробиць, варто поставити запитання: «Навіщо взагалі тестувати інфраструктуру?» Для цього є багато причин і деякі з них:
- Модульне тестування окремих функцій чи фрагментів логіки вашої програми
- Перевіряє бажаний стан інфраструктури на відповідність певним обмеженням.
- Виявлення поширених помилок, таких як відсутність шифрування storage bucket або незахищений відкритий доступ з Інтернету до віртуальних машин.
- Перевірка виконання провіженінгу інфраструктури.
- Виконує runtime-тестування логіки програми, запущеної всередині вашої «запрограмованої» інфраструктури для перевірки працездатності після просування.
- Як бачимо, є широкий спектр варіантів тестування інфраструктури. Polumi має механізми для тестування в кожній точці цього спектру. Почнемо і подивимося як це працює.
Модульне тестування
Pulumi-програми створюються мовами програмування загального призначення, таких як JavaScript, Python, TypeScript чи Go. Тому їм доступна повна міць цих мов, включаючи їх інструментарій і бібліотеки, зокрема тестові фреймворки. Pulumi є мультихмарним, що означає можливість використання для тестів будь-яких хмарних провайдерів.
(У цій статті, незважаючи на мультимовність та мультихмарність, ми використовуємо JavaScript та Mocha та орієнтуємося на AWS. Можна використовувати Python unittest
, тестовий фрейморк Go або будь-який інший улюблений тестовий фреймворк. І, звичайно, Pulumi відмінно працює з Azure, Google Cloud, Kubernetes.)
Як ми бачили, є кілька причин, з яких може знадобитися тестувати ваш інфраструктурний код. Однією з них є стандартне модульне тестування. Оскільки ваш код може мати функції, наприклад, для обчислення CIDR, динамічного обчислення імен, тегів і т.д. — ви, мабуть, захочете протестувати. Це те ж саме, що написання звичайних модульних тестів для додатків улюбленою мовою програмування.
Якщо трохи ускладнити, можна перевірити, як ваша програма розподіляє ресурси. Для ілюстрації давайте уявімо, що нам потрібно створити простий EC2-сервер і ми хочемо бути впевнені в наступному:
- Інстанси мають тег
Name
. - Інстанси не повинні використовувати inline-скрипт
userData
— ми маємо використовувати AMI (образ). - Не повинно бути SSH, відкритий в Інтернеті.
Цей приклад написаний за мотивами
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;
Це базова програма Pulumi: вона просто алокує групу безпеки EC2 та інстанс. Однак слід зазначити, що тут ми порушуємо всі три правила, викладені вище. Давайте писати тести!
Пишемо тести
Загальна структура наших тестів буде виглядати як звичайні тести 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, открытого в Интернет.
});
});
Тепер давайте напишемо наш перший тест: переконаємося, що екземпляри мають тег Name
. Для перевірки цього ми просто отримуємо об'єкт EC2-інстансу та перевіряємо відповідну властивість 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();
}
});
});
Виглядає як звичайний тест, але з кількома особливостями, що заслуговують на увагу:
- Оскільки ми запитуємо стан ресурсу перед розгортанням, наші тести завжди виконуються в режимі ”plan” (або ”preview”). Таким чином, існує багато властивостей, значення яких просто не будуть отримані або не визначені. Сюди входять усі вихідні властивості, що обчислюються вашим хмарним провайдером. Для наших тестів це нормально – ми перевіряємо лише вхідні дані. До цього питання ми ще повернемося пізніше, коли дійде до інтеграційних тестів.
- Так як всі властивості Pulumi-ресурсів є «виходами», і багато хто з них обчислюється асинхронно, то нам необхідно використовувати метод apply, щоб отримати доступ до значень. Це дуже схоже на проміси та афункцію
then
. - Оскільки ми використовуємо кілька властивостей для того, щоб показати URN ресурсу в повідомленні про помилку, нам потрібно використовувати функцію
pulumi.all
, щоб об'єднати їх. - Нарешті, оскільки ці значення обчислюються асинхронно, нам потрібно використовувати вбудовану асинхронну можливість Mocha зі зворотним викликом
done
або поверненням промісу.
Після того, як ми всі налаштуємо, ми матимемо доступ до вхідних даних як до простих значень JavaScript. Властивість tags
- це map (асоціативний масив), тому ми просто переконаємося, що це (1) не false, і (2) є ключем для Name
. Це дуже просто, і тепер ми можемо перевірити все, що завгодно!
Тепер напишемо нашу другу перевірку. Це ще простіше:
// 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();
}
});
});
І нарешті напишемо третій тест. Це буде трохи складніше, тому що ми шукаємо правила входу, пов'язані з групою безпеки, яких може бути багато, та діапазони CIDR у цих правилах, яких також може бути багато. Але ми впоралися:
// 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();
}
});
});
От і все. Тепер запустимо тести!
Запуск тестів
Запускати тести, в більшості випадків, можна звичайним способом, використовуючи вибраний тестовий фреймворк. Але є одна особливість Pulumi, на яку варто звернути увагу.
Зазвичай для запуску Pulumi-програм використовується pulimi CLI (Command Line interface, інтерфейс командного рядка), який налаштовує runtime мови, контролює запуски Pulumi движка, щоб можна було фіксувати операції з ресурсами і включити їх в план і т.д. Проте є одна проблема. При запуску під контролем тестового фреймворку не буде зв'язку між CLI і двигуном Pulumi.
Щоб оминути цю проблему, нам просто потрібно вказати таке:
- Ім'я проекту, що міститься у змінній оточенні
PULUMI_NODEJS_PROJECT
(або, у більш загальному випадку,PULUMI__PROJECT для других языков).
Ім'я стека, яке вказано у змінній оточенніPULUMI_NODEJS_STACK
(або, у більш загальному випадку,PULUMI__ STACK).
Ваші змінні конфігурації стека. Вони можуть бути отримані за допомогою змінного оточенняPULUMI_CONFIG
та їх формат є JSON map з парами ключ/значення.Програма видасть попередження, що під час виконання недоступне з'єднання з CLI/engine. Це важливо, тому що насправді ваша програма нічого не буде розгортати і це може стати сюрпризом, якщо це не те, що ви хотіли зробити! Щоб сказати Pulumi, що саме те, що вам потрібно, ви можете встановити
PULUMI_TEST_MODE
вtrue
.Уявіть собі, що нам потрібно вказати назву проекту в
my-ws
, назва стекаdev
, та регіон AWSus-west-2
. Командний рядок для виконання Mocha-тестів буде виглядати так:$ PULUMI_TEST_MODE=true PULUMI_NODEJS_STACK="my-ws" PULUMI_NODEJS_PROJECT="dev" PULUMI_CONFIG='{ "aws:region": "us-west-2" }' mocha tests.js
Виконання цього, як і очікувалося, покаже нам, що у нас є три тести, що впали!
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
Давайте виправимо нашу програму:
"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;
А потім повторно запустимо тести:
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)
Все пройшло успішно... Ура! ✓✓✓
На сьогодні все, а про тестування розгортання поговоримо у другій частині перекладу 😉
Джерело: habr.com