การทดสอบโครงสร้างพื้นฐานเป็นโค้ดด้วย Pulumi ส่วนที่ 1

สวัสดีตอนบ่ายเพื่อน โดยคาดว่าจะเริ่มมีกระแสใหม่ในอัตราดังกล่าว "แนวทางปฏิบัติและเครื่องมือ DevOps" เรากำลังแบ่งปันคำแปลใหม่กับคุณ ไป.

การทดสอบโครงสร้างพื้นฐานเป็นโค้ดด้วย Pulumi ส่วนที่ 1

การใช้ Pulumi และภาษาการเขียนโปรแกรมทั่วไปสำหรับโค้ดโครงสร้างพื้นฐาน (โครงสร้างพื้นฐานเป็นโค้ด) ให้ข้อดีหลายประการ: ความพร้อมใช้งานของทักษะและความรู้ การขจัดส่วนสำเร็จรูปในโค้ดผ่านทางนามธรรม เครื่องมือที่ทีมของคุณคุ้นเคย เช่น IDE และ linters เครื่องมือทางวิศวกรรมซอฟต์แวร์ทั้งหมดนี้ไม่เพียงแต่ทำให้เรามีประสิทธิผลมากขึ้นเท่านั้น แต่ยังช่วยปรับปรุงคุณภาพของโค้ดของเราด้วย ดังนั้นจึงเป็นเรื่องธรรมดาที่การใช้ภาษาโปรแกรมทั่วไปช่วยให้เราสามารถแนะนำแนวทางปฏิบัติในการพัฒนาซอฟต์แวร์ที่สำคัญอีกอย่างหนึ่งได้ - การทดสอบ.

ในบทความนี้ เราจะดูว่า Pulumi ช่วยเราทดสอบโครงสร้างพื้นฐานตามโค้ดของเราได้อย่างไร

การทดสอบโครงสร้างพื้นฐานเป็นโค้ดด้วย Pulumi ส่วนที่ 1

ทำไมต้องทดสอบโครงสร้างพื้นฐาน?

ก่อนที่จะลงรายละเอียด ควรถามคำถาม: “ทำไมต้องทดสอบโครงสร้างพื้นฐานเลย” มีสาเหตุหลายประการสำหรับเรื่องนี้ และนี่คือสาเหตุบางส่วน:

  • การทดสอบหน่วยของแต่ละฟังก์ชันหรือส่วนของลอจิกโปรแกรมของคุณ
  • ตรวจสอบสถานะโครงสร้างพื้นฐานที่ต้องการโดยเทียบกับข้อจำกัดบางประการ
  • การตรวจจับข้อผิดพลาดทั่วไป เช่น การขาดการเข้ารหัสที่เก็บข้อมูลหรือการเข้าถึงแบบเปิดจากอินเทอร์เน็ตไปยังเครื่องเสมือน
  • ตรวจสอบการดำเนินการจัดเตรียมโครงสร้างพื้นฐาน
  • ทำการทดสอบรันไทม์ของตรรกะของแอปพลิเคชันที่ทำงานภายในโครงสร้างพื้นฐาน "ที่ตั้งโปรแกรมไว้" ของคุณเพื่อตรวจสอบฟังก์ชันการทำงานหลังจากการจัดเตรียม
  • ดังที่เราเห็นแล้วว่ามีตัวเลือกการทดสอบโครงสร้างพื้นฐานที่หลากหลาย Polumi มีกลไกในการทดสอบทุกจุดบนสเปกตรัมนี้ มาเริ่มต้นและดูว่ามันทำงานอย่างไร

การทดสอบหน่วย

โปรแกรม Pulumi เขียนด้วยภาษาโปรแกรมอเนกประสงค์ เช่น JavaScript, Python, TypeScript หรือ Go ดังนั้นภาษาเหล่านี้จึงมีประสิทธิภาพเต็มที่ รวมถึงเครื่องมือและไลบรารี รวมถึงเฟรมเวิร์กการทดสอบด้วย Pulumi เป็นมัลติคลาวด์ ซึ่งหมายความว่าสามารถใช้สำหรับการทดสอบจากผู้ให้บริการคลาวด์รายใดก็ได้

(ในบทความนี้ แม้ว่าจะเป็นหลายภาษาและหลายคลาวด์ แต่เราใช้ JavaScript และ Mocha และมุ่งเน้นไปที่ AWS คุณสามารถใช้ Python unittest, Go test framework หรือ test framework อื่นๆ ที่คุณชอบ และแน่นอนว่า Pulumi ทำงานได้ดีกับ Azure, Google Cloud, Kubernetes)

ตามที่เราได้เห็นแล้ว มีสาเหตุหลายประการที่คุณอาจต้องการทดสอบโค้ดโครงสร้างพื้นฐานของคุณ หนึ่งในนั้นคือการทดสอบหน่วยแบบธรรมดา เนื่องจากโค้ดของคุณอาจมีฟังก์ชันต่างๆ เช่น ในการคำนวณ CIDR คำนวณชื่อ แท็ก ฯลฯ แบบไดนามิก - คุณอาจต้องการทดสอบพวกเขา สิ่งนี้เหมือนกับการเขียนการทดสอบหน่วยปกติสำหรับแอปพลิเคชันในภาษาการเขียนโปรแกรมที่คุณชื่นชอบ
เพื่อให้ซับซ้อนขึ้นอีกเล็กน้อย คุณสามารถตรวจสอบวิธีที่โปรแกรมของคุณจัดสรรทรัพยากรได้ เพื่อแสดงให้เห็น สมมติว่าเราจำเป็นต้องสร้างเซิร์ฟเวอร์ EC2 แบบธรรมดา และเราต้องการความมั่นใจในสิ่งต่อไปนี้:

  • อินสแตนซ์มีแท็ก Name.
  • อินสแตนซ์ไม่ควรใช้สคริปต์อินไลน์ userData - เราต้องใช้ AMI (รูปภาพ)
  • ไม่ควรมีการเปิดเผย SSH บนอินเทอร์เน็ต

ตัวอย่างนี้มีพื้นฐานมาจาก ตัวอย่างของฉัน aws-js-webserver:

ดัชนี.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 และอินสแตนซ์ อย่างไรก็ตาม ควรสังเกตว่าที่นี่เรากำลังฝ่าฝืนกฎทั้งสามข้อที่ระบุไว้ข้างต้น มาเขียนแบบทดสอบกันเถอะ!

การทดสอบการเขียน

โครงสร้างทั่วไปของการทดสอบของเราจะมีลักษณะเหมือนกับการทดสอบมอคค่าทั่วไป:

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();
                }
            });
        });

ดูเหมือนเป็นการทดสอบปกติ แต่มีคุณสมบัติบางประการที่น่าสังเกต:

  • เนื่องจากเราสอบถามสถานะของทรัพยากรก่อนที่จะปรับใช้ การทดสอบของเราจึงทำงานในโหมด "วางแผน" (หรือ "แสดงตัวอย่าง") เสมอ ดังนั้นจึงมีคุณสมบัติหลายอย่างที่มีค่าจะไม่สามารถเรียกคืนหรือไม่สามารถกำหนดได้ ซึ่งรวมถึงคุณสมบัติเอาต์พุตทั้งหมดที่คำนวณโดยผู้ให้บริการคลาวด์ของคุณ นี่เป็นเรื่องปกติสำหรับการทดสอบของเรา - เราจะตรวจสอบเฉพาะข้อมูลที่ป้อนเข้าเท่านั้น เราจะกลับมาที่ปัญหานี้ในภายหลัง เมื่อพูดถึงการทดสอบการรวมระบบ
  • เนื่องจากคุณสมบัติทรัพยากร Pulumi ทั้งหมดเป็นเอาต์พุต และคุณสมบัติส่วนใหญ่ได้รับการประเมินแบบอะซิงโครนัส เราจึงจำเป็นต้องใช้วิธี Apply เพื่อเข้าถึงค่า สิ่งนี้คล้ายกันมากกับคำสัญญาและการกระทำผิด then .
  • เนื่องจากเราใช้คุณสมบัติหลายอย่างเพื่อแสดงทรัพยากร URN ในข้อความแสดงข้อผิดพลาด เราจึงจำเป็นต้องใช้ฟังก์ชันนี้ pulumi.allเพื่อรวมเข้าด้วยกัน
  • สุดท้ายนี้ เนื่องจากค่าเหล่านี้คำนวณแบบอะซิงโครนัส เราจึงจำเป็นต้องใช้ฟีเจอร์การโทรกลับแบบอะซิงโครนัสในตัวของ Mocha done หรือคืนคำสัญญา

เมื่อเราตั้งค่าทุกอย่างแล้ว เราจะสามารถเข้าถึงอินพุตเป็นค่า JavaScript ธรรมดาได้ คุณสมบัติ tags คือแผนที่ (อาเรย์แบบเชื่อมโยง) ดังนั้นเราจะตรวจสอบให้แน่ใจว่ามัน (1) ไม่ใช่เท็จ และ (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 (อินเทอร์เฟซบรรทัดคำสั่ง) ซึ่งจะกำหนดค่ารันไทม์ภาษา ควบคุมการเปิดตัวกลไก Pulumi เพื่อให้สามารถบันทึกและรวมการดำเนินการกับทรัพยากรไว้ในแผนได้ ฯลฯ อย่างไรก็ตามมีปัญหาหนึ่งประการ เมื่อทำงานภายใต้การควบคุมของกรอบการทดสอบของคุณ จะไม่มีการสื่อสารระหว่าง CLI และเครื่องยนต์ Pulumi

เพื่อแก้ไขปัญหานี้ เราเพียงแค่ต้องระบุสิ่งต่อไปนี้:

  • ชื่อโปรเจ็กต์ซึ่งมีอยู่ในตัวแปรสภาพแวดล้อม PULUMI_NODEJS_PROJECT (หรือโดยทั่วไปแล้ว PULUMI__PROJECT для других языков).
    ชื่อของสแต็กที่ระบุในตัวแปรสภาพแวดล้อม PULUMI_NODEJS_STACK (หรือโดยทั่วไปแล้ว PULUMI__ STACK).
    ตัวแปรการกำหนดค่าสแต็กของคุณ สามารถรับได้โดยใช้ตัวแปรสภาพแวดล้อม PULUMI_CONFIG และรูปแบบคือแมป JSON พร้อมคู่คีย์/ค่า

    โปรแกรมจะออกคำเตือนเพื่อระบุว่าการเชื่อมต่อกับ CLI/เอ็นจิ้นไม่พร้อมใช้งานระหว่างการดำเนินการ นี่เป็นสิ่งสำคัญเนื่องจากโปรแกรมของคุณจะไม่มีการปรับใช้อะไรเลย และอาจเป็นเรื่องที่น่าแปลกใจหากนั่นไม่ใช่สิ่งที่คุณตั้งใจจะทำ! หากต้องการบอก 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)

    ทุกอย่างเป็นไปด้วยดี... ไชโย! ✓✓✓

    นั่นคือทั้งหมดสำหรับวันนี้ แต่เราจะพูดถึงการทดสอบการปรับใช้ในส่วนที่สองของการแปล 😉

ที่มา: will.com

เพิ่มความคิดเห็น