Добрый день, друзья. В преддверии старта нового потока по курсу
Использование 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