Тэставанне інфраструктуры як код з дапамогай 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

Дадаць каментар