Тестування інфраструктури як код за допомогою Pulumi. Частина 1

Добрий день друзі. Напередодні старту нового потоку за курсом «DevOps практики та інструменти» ділимося з вами новим перекладом. Поїхали.

Тестування інфраструктури як код за допомогою Pulumi. Частина 1

Використання Pulumi та мов програмування загального призначення для інфраструктурного коду (Infrastructure as Code) дає багато переваг: наявність навичок та знань, усунення в коді бойлерплейту через абстракцію, знайомі вашій команді інструменти, такі як IDE та лінтери. Всі ці інструменти програмної інженерії не тільки роблять нас більш продуктивними, але й покращують якість коду. Тому цілком природно, що використання мов програмування загального призначення дозволяє впровадити ще одну важливу практику розробки програмного забезпечення. тестування.

У цій статті ми розглянемо, як Pulumi допомагає тестувати нашу «інфраструктуру як код».

Тестування інфраструктури як код за допомогою Pulumi. Частина 1

Навіщо випробувати інфраструктуру?

Перш ніж вдаватися до подробиць, варто поставити запитання: «Навіщо взагалі тестувати інфраструктуру?» Для цього є багато причин і деякі з них:

  • Модульне тестування окремих функцій чи фрагментів логіки вашої програми
  • Перевіряє бажаний стан інфраструктури на відповідність певним обмеженням.
  • Виявлення поширених помилок, таких як відсутність шифрування 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, відкритий в Інтернеті.

Цей приклад написаний за мотивами мого прикладу 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 ], // 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, та регіон AWS us-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

Додати коментар або відгук