Hogyan teszteljük az infrastruktúrát mint kódot
Megosztás
Összefoglalót olvas. A teljes tartalom itt érhető el: pulumi.com.
Az infrastruktúra kód (IaC) tesztelése azt jelenti, hogy az infrastruktúra kódját ugyanúgy érvényesítjük, ahogy az alkalmazás szoftverét teszteljük—egységtesztek a hamisított felhőszolgáltatókkal, amelyek milliszekundumok alatt futnak, integrációs tesztek, amelyek valós erőforrásokat telepítenek és ellenőriznek, valamint szabályellenőrzések, amelyek minden előnézet és telepítés során alkalmazzák a megfelelőségi szabályokat. Ezek a rétegek együtt észlelik a hibás konfigurációkat, mielőtt azok a termelésbe kerülnének. A tesztelés nélküli infrastruktúra kód felelősséget jelent. Egy hiányzó címke a biztonsági csoport szabályában, egy nyilvános olvasási hozzáféréssel rendelkező S3 tároló, egy hibásan konfigurált IAM politika—bármelyikük átsiklik a kódellenőrzésen és bekerül a termelésbe. Az infrastruktúra hibák általában nehezen hibakereshetőek, mint az alkalmazás hibák, mert a visszajelzési ciklus lassú (telepítés, megfigyelés, megsemmisítés) és a hatásmennyiség nagy (kiesés, biztonsági incidens, váratlan AWS számla). A jó hír: a Pulumi általános célú programozási nyelvekre épül, ami azt jelenti, hogy az infrastruktúra kódot éppen azokkal az eszközökkel és keretrendszerekkel tesztelhetjük, amelyeket már most is az alkalmazáskód tesztelésére használsz. Nincs új nyelvet tanulni. Nincs külön eszközkészlet. Csak pytest, Mocha vagy go test—mutatva az infrastruktúra programodra. Ez az útmutató bemutatja mindhárom tesztelési réteget teljes, futtatható példákkal. Miért kellene infrastruktúrát tesztelni kódként? Az alkalmazás kód tesztelésének esete jól megalapozott. Az IaC tesztelésének esete legalább olyan erős—vitathatóan erősebb, mert a nem észlelt hibák következményei súlyosabbak. Észlelni a hibás konfigurációkat, mielőtt azok telepítésre kerülnek. Egy egységteszt, amely ellenőrzi, hogy egy biztonsági csoport engedélyezi-e az SSH-t a 0.0.0.0/0-ból, milliszekundumok alatt észleli a hibás konfigurációt, mielőtt bármilyen felhő API-hívás megtörténne. Ugyanez a teszt a CI-ben észleli, mielőtt a kódot egyesítenék. Megakadályozza a eltéréseket és kieséseket. Az integrációs tesztek, amelyek egy színpadkörnyezetbe telepítenek és validálják a futásbeli viselkedést—visszaadja-e a HTTP végpont a 200-at? fogadja-e a kapcsolatokat az adatbázis?—bizalmat adnak abban, hogy egy infrastruktúra változás nem csendben rontja el a függő szolgáltatást. Biztonságosan átalakítható. A jól tesztelt infrastruktúra kód átstrukturálható, moduláris formába hozható és fejleszthető félelem nélkül. Amikor az összes tulajdonságodat, címkéidet és biztonsági szabályaidat tesztekben rögzíted, azonnal tudod, ha egy átalakítás megszegi a kényszert. Automatikusan érvényesíti a biztonságot és a megfelelőséget. A szabályok kód formájában lehetővé teszik, hogy a szervezeted biztonsági szabályait végrehajtható tesztekké alakítsd. A kötelező politikák teljesen blokkolják a nem megfelelőségeket; az ajánlási politikák figyelmeztetéseket emelnek ki az emberi felülvizsgálatra. Ezek a szabályok minden pulumi előnézet és pulumi up során futnak—nem csak audit időszakban. Tartalmazza az infrastruktúrát szoftvergyorsasággal. Azok a csapatok, akik tesztelik az IaC-t, gyorsabban szállítanak változásokat, kevesebb félelemmel. A CI csővezeték bizalmi gépezetté válik, nem pedig telepítési kapu. Az éves DORA DevOps állapotfelmérési kutatás folyamatosan társul egy mintához az elit teljesítményű mérnöki csapatok között—azok, akik gyakran telepítenek és gyorsan helyreállnak a hibákból: átfogó automatizált tesztelés az egész halmazban. Az infrastruktúra már nem mentes ettől a szabványtól. Milyen típusú IaC tesztek léteznek? Az IaC tesztelés a klasszikus szoftver teszt piramist tükrözi: széles alapú gyors, olcsó egységtesztek, egy kisebb réteg integrációs tesztek valós infrastruktúrával, és politika ellenőrzések, amelyek mindenütt érvényesítik a szabályokat. Réteg Miért tesztel? Sebesség Költség Felhői hitelesítő adatok? Egységtesztek Erőforrás tulajdonságok, címkék, biztonsági szabályok, névadási konvenciók Milliszekundumok Ingyenes Nem Integrációs tesztek Teljes halozati életciklus, futásbeli viselkedés, kereszt-eredményfüggőségek Percek Valódi felhőkiadás Igen Politika kód formájában Megfelelőségi szabályok, biztonsági alapok, szervezeti szabványok Milliszekundumok Ingyenes Nem (előnézet időszakban fut) Kezdj az egységtesztekkel—ezek ingyenesek, gyorsak és észlelik a leggyakoribb hibákat. Adj hozzá integrációs teszteket a kritikus utakon (a telepítési csővezeték, adat infrastruktúra, biztonságérzékeny erőforrások). Építs be politika kódot a megfelelőségi követelményekhez, amelyek sosem csökkenhetnek. Hogyan írhatók egységtesztek a Pulumi esetében? A Pulumi egységtesztek a Pulumi program és a felhőszolgáltató közötti kommunikációs csatornát hamisítványokkal helyettesítik. Amikor a program egy erőforrást regisztrál (pl. aws.ec2.SecurityGroup(...)), a hamisítvány megakadályozza a hívást, hamis állapotot ad vissza, és soha nem végez el valódi felhő API hívást. A tesztek ugyanabban a folyamatban, ugyanabban a nyelven, a már használt tesztfuttatóval futnak. A hamisítványok beállítása A beimportálási sorrend szabálya: A hamisítványokat a Pulumi program importálása előtt kell beállítani. Ha először importálod a programot, a Pulumi futásideje hamisítványok nélkül inicializálódik és valós felhőhívásokat fog végezni. # test_infra.py import unittest import pulumi class MyMocks(pulumi.runtime.Mocks): def new_resource(self, args: pulumi.runtime.MockResourceArgs): # Visszatér [id, állapot]. Az állapot kulcsainak camelCase formátumúaknak kell lenniük. return [args.name + "_id", args.inputs] def call(self, args: pulumi.runtime.MockCallArgs): return {} # A hamisítványokat A PROGRAM BEIMPORTÁLÁSA ELŐTT KELL BEÁLLÍTANI pulumi.runtime.set_mocks(MyMocks()) import infra # A programot A HAMISÍTÓK BEÁLLÍTÁSA UTÁN IMPORTÁLD class TestInfra(unittest.TestCase): @pulumi.runtime.test def test_no_public_ssh(self): """A biztonsági csoportnak nem szabad kitenni az SSH-t a nyilvános interneten.""" def check_security_group(args): ingress_rules = args[0] for rule in (ingress_rules or []): for cidr in (rule.get("cidrBlocks") or []): self.assertNotEqual( cidr, "0.0.0.0/0", "A biztonsági csoportnak nem szabad kitenni a 22-es portot az internetre." ) return pulumi.Output.all(infra.group.ingress).apply(check_security_group) @pulumi.runtime.test def test_server_has_tags(self): """Az EC2 példánynak tartalmaznia kell a szükséges címkéket.""" def check_tags(args): tags = args[0] self.assertIn("Environment", tags, "Hiányzó 'Environment' címke") self.assertIn("Name", tags, "Hiányzó 'Name' címke") return pulumi.Output.all(infra.server.tags).apply(check_tags) if __name__ == "__main__": unittest.main() Futtatás: python -m unittest discover -v // infra.test.ts import * as pulumi from "@pulumi/pulumi"; import { expect } from "chai"; // A hamisítványokat A PROGRAM DINAMIKUS BEIMPORTÁLÁSA ELŐTT KELL BEÁLLÍTANI pulumi.runtime.setMocks( { newResource: (args: pulumi.runtime.MockResourceArgs): { id: string; state: any } => { // Az állapot kulcsainak camelCase formátumúnak kell lenniük return { id: `${args.name}-id`, state: args.inputs }; }, call: (args: pulumi.runtime.MockCallArgs) => ({ ...args.inputs }), }, "my-project", "test", false // előnézet = false ); describe("Infrastruktúra", function () { let infra: typeof import("./index"); before(async function () { // Dinamikus import A HAMISÍTÓK BEÁLLÍTÁSA UTÁN infra = await import("./index"); }); it("nem szabad kitenni az SSH-t az internetre", function (done) { pulumi.all([infra.group.ingress]).apply(([ingress]) => { for (const rule of ingress ?? []) { for (const cidr of rule.cidrBlocks ?? []) { expect(cidr).to.not.equal( "0.0.0.0/0", "A biztonsági csoportnak nem szabad kitenni a 22-es portot az internetre" ); } } done(); }); }); it("kell, hogy legyenek szükséges címkék", function (done) { pulumi.all([infra.server.tags]).apply(([tags]) => { expect(tags).to.include.keys("Environment", "Name"); done(); }); }); }); Futtatás: mocha -r ts-node/register infra.test.ts // infra_test.go package infra_test import ( "testing" "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/stretchr/testify/assert" ) // MockResourceMonitor implementálja a pulumi.ResourceMonitor-t az egységtesztekhez. func TestNoPublicSSH(t *testing.T) { err := pulumi.RunErr(func(ctx *pulumi.Context) error { infra, err := NewInfra(ctx) assert.NoError(t, err) infra.Group.Ingress.ApplyT(func(rules []ec2.SecurityGroupIngress) error { for _, rule := range rules { for _, cidr := range rule.CidrBlocks { assert.NotEqual(t, "0.0.0.0/0", cidr, "A biztonsági csoportnak nem szabad kitenni az SSH-t a nyilvános internetre") } } return nil }) return nil }, pulumi.WithMocks("my-project", "test", mocks{})) assert.NoError(t, err) } type mocks struct{} func (mocks) NewResource(args pulumi.MockResourceArgs) (string, map[string]interface{}, error) { return args.Name + "_id", args.Inputs, nil } func (mocks) Call(args pulumi.MockCallArgs) (map[string]interface{}, error) { return args.Args, nil } Futtatás: go test ./... -v // InfraTests.cs using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Pulumi; using Pulumi.Testing; using Xunit; public class InfraTests { [Fact] public async Task NoPublicSSH() { var resources = await Testing.RunAsync<MyStack>(); var sg = resources.OfType<Pulumi.Aws.Ec2.SecurityGroup>().First(); var ingress = await sg.Ingress.GetValueAsync(); foreach (var rule in ingress) { foreach (var cidr in rule.CidrBlocks) { Assert.NotEqual("0.0.0.0/0", cidr); } } } [Fact] public async Task ServerHasRequiredTags() { var resources = await Testing.RunAsync<MyStack>(); var server = resources.OfType<Pulumi.Aws.Ec2.Instance>().First(); var tags = await server.Tags.GetValueAsync(); Assert.True(tags.ContainsKey("Environment"), "Hiányzó 'Environment' címke"); Assert.True(tags.ContainsKey("Name"), "Hiányzó 'Name' címke"); } } Futtatás: dotnet test // InfraTest.java package myproject; import com.pulumi.aws.ec2.Instance; import com.pulumi.aws.ec2.SecurityGroup; import com.pulumi.test.Mocks; import com.pulumi.test.Mocks.CallArgs; import com.pulumi.test.Mocks.ResourceArgs; import com.pulumi.test.Mocks.ResourceResult; import com.pulumi.test.PulumiTest; import com.pulumi.test.TestOptions; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import java.util.HashMap; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import static org.junit.jupiter.api.Assertions.*; class InfraTest { // A hívások interceptálásához használj hamisítványokat. Az állapot kulcsainak camelCase formátumnak kell lenniük. static class MyMocks implements Mocks { @Override public CompletableFuture<ResourceResult> newResourceAsync(ResourceArgs args) { var state = new HashMap<>(args.inputs); return CompletableFuture.completedFuture( ResourceResult.of(Optional.of(args.name + "_id"), state)); } @Override public CompletableFuture<Map<String, Object>> callAsync(CallArgs args) { return CompletableFuture.completedFuture(Map.of()); } } // A PulumiTest.cleanup() minden teszt után futnia kell, hogy visszaállítsa a futásidejű állapotot. @AfterEach void cleanup() { PulumiTest.cleanup(); } @Test void serverHasRequiredTags() { var result = PulumiTest .withMocks(new MyMocks()) .withOptions(TestOptions.builder() .projectName("project").stackName("stack").preview(false) .build()) .runTest(ctx -> new Infra()); var instance = result.resources().stream() .filter(r -> r instanceof Instance) .map(r -> (Instance) r) .findFirst() .orElseThrow(() -> new AssertionError("EC2 példány nem található")); var tags = PulumiTest.extractValue(instance.tags()) .orElseThrow(() -> new AssertionError("A címkék nem lehetnek nullák")); assertTrue(tags.containsKey("Environment"), "Hiányzó 'Environment' címke"); assertTrue(tags.containsKey("Name"), "Hiányzó 'Name' címke"); } @Test void noPublicSshExposed() { var result = PulumiTest .withMocks(new MyMocks()) .withOptions(TestOptions.builder() .projectName("project").stackName("stack").preview(false) .build()) .runTest(ctx -> new Infra()); var group = result.resources().stream() .filter(r -> r instanceof SecurityGroup) .map(r -> (SecurityGroup) r) .findFirst() .orElseThrow(() -> new AssertionError("Biztonsági csoport nem található")); var rules = PulumiTest.extractValue(group.ingress()); for (var rule : rules) { for (var cidr : rule.cidrBlocks()) { assertNotEquals("0.0.0.0/0", cidr, "A biztonsági csoportnak nem szabad kitenni az SSH-t a nyilvános internetre"); } } } } Futtatás: mvn test (JUnit 5) A tesztelendő program Mindkét példa ezt a programot teszteli: # infra.py import pulumi from pulumi_aws import ec2 group = ec2.SecurityGroup("web-secgrp", ingress=[{ "protocol": "tcp", "from_port": 80, "to_port": 80, "cidrBlocks": ["0.0.0.0/0"], }], ) server = ec2.Instance("web-server", instance_type="t2.micro", ami="ami-0b0ea68c435eb488d", vpc_security_group_ids=[group.id], tags={"Name": "web-server", "Environment": "dev"}, ) // index.ts import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; export const group = new aws.ec2.SecurityGroup("web-secgrp", { ingress: [{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"], }], }); export const server = new aws.ec2.Instance("web-server", { instanceType: "t2.micro", ami: "ami-0b0ea68c435eb488d", vpcSecurityGroupIds: [group.id], tags: { Name: "web-server", Environment: "dev" }, }); // main.go package infra_test import ( "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/ec2" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" ) type Infra struct { Group *ec2.SecurityGroup Server *ec2.Instance } func NewInfra(ctx *pulumi.Context) (*Infra, error) { group, err := ec2.NewSecurityGroup(ctx, "web-secgrp", &ec2.SecurityGroupArgs{ Ingress: ec2.SecurityGroupIngressArray{ ec2.SecurityGroupIngressArgs{ Protocol: pulumi.String("tcp"), FromPort: pulumi.Int(80), ToPort: pulumi.Int(80), CidrBlocks: pulumi.StringArray{pulumi.String("0.0.0.0/0")}, }, }, }) if err != nil { return nil, err } server, err := ec2.NewInstance(ctx, "web-server", &ec2.InstanceArgs{ InstanceType: pulumi.String("t2.micro"), Ami: pulumi.String("ami-0b0ea68c435eb488d"), VpcSecurityGroupIds: pulumi.StringArray{group.ID()}, Tags: pulumi.StringMap{ "Name": pulumi.String("web-server"), "Environment": pulumi.String("dev"), }, }) if err != nil { return nil, err } return &Infra{Group: group, Server: server}, nil } // MyStack.cs using Pulumi; using Pulumi.Aws.Ec2; using Pulumi.Aws.Ec2.Inputs; public class MyStack : Stack { public MyStack() { var group = new SecurityGroup("web-secgrp", new SecurityGroupArgs { Ingress = new[] { new SecurityGroupIngressArgs { Protocol = "tcp", FromPort = 80, ToPort = 80, CidrBlocks = new[] { "0.0.0.0/0" }, }, }, }); var server = new Instance("web-server", new InstanceArgs { InstanceType = "t2.micro", Ami = "ami-0b0ea68c435eb488d", VpcSecurityGroupIds = new[] { group.Id }, Tags = new InputMap<string> { { "Name", "web-server" }, { "Environment", "dev"}, }, }); } } // Infra.java package myproject; import com.pulumi.Context; import com.pulumi.Pulumi; import com.pulumi.aws.ec2.SecurityGroup; import com.pulumi.aws.ec2.SecurityGroupArgs; import com.pulumi.aws.ec2.Instance; import com.pulumi.aws.ec2.InstanceArgs; import com.pulumi.aws.ec2.inputs.SecurityGroupIngressArgs; import java.util.List; import java.util.Map; public class Infra { public final SecurityGroup group; public final Instance server; public Infra() { this.group = new SecurityGroup("web-secgrp", SecurityGroupArgs.builder() .ingress(SecurityGroupIngressArgs.builder() .protocol("tcp") .fromPort(80) .toPort(80) .cidrBlocks("0.0.0.0/0") .build()) .build()); this.server = new Instance("web-server", InstanceArgs.builder() .instanceType("t2.micro") .ami("ami-0b0ea68c435eb488d") .vpcSecurityGroupIds(group.id().applyValue(List::of)) .tags(Map.of("Name", "web-server", "Environment", "dev")) .build()); } } Kulcsfontosságú részlet: A hamisítványok new_resource/newResource visszatérési értékében a tulajdonság kulcsainak camelCase formátumúnak kell lenniük (cidrBlocks, nem cidr_blocks), függetlenül attól, hogy melyik nyelven írsz. Hogyan futtathatsz integrációs teszteket valós felhő erőforrásokkal? Az egységtesztek megmondják, hogy a te erőforrásdefiníciód helyes-e. Az integrációs tesztek megmondják, hogy az infrastruktúrád ténylegesen működik-e. Az integrációs tesztek valós felhő erőforrásokat telepítenek, ellene tesztelnek, majd mindent megsemmisítenek—teljeskörű bizalom árán, valódi felhőköltséggel és idővel. Az Automatizációs API használata A Pulumi Automatizációs API lehetővé teszi, hogy a pulumi up, pulumi destroy és minden más Pulumi CLI műveletet programozottan végezzen el, egy tesztből. A verem definiálható inline (egy funkció használatával, amely futtatja a Pulumi programot) vagy irányítható egy létező projekt könyvtárba. # test_integration.py import pytest import pulumi import pulumi_aws as aws from pulumi import automation as auto def create_bucket_program(): """A Pulumi program, amelyet a teszt során telepítenek.""" bucket = aws.s3.Bucket("test-bucket", tags={"Environment": "test", "ManagedBy": "pulumi"}, ) pulumi.export("bucket_name", bucket.id) pulumi.export("bucket_arn", bucket.arn) @pytest.fixture(scope="module") def deployed_stack(): """Telepíti a veremet a tesztmodulhoz és megsemmisíti, amikor kész.""" stack = auto.create_or_select_stack( stack_name="integration-test", project_name="bucket-test", program=create_bucket_program, ) stack.set_config("aws:region", auto.ConfigValue(value="us-west-2")) # Telepítse a bővítményeket stack.workspace.install_plugin("aws", "v7.34.0") # Telepíteni up_result = stack.up(on_output=print) yield up_result.outputs # Teardown: mindent megsemmisíteni a tesztmódú befejezése után stack.destroy(on_output=print) stack.workspace.remove_stack("integration-test") def test_bucket_was_created(deployed_stack): """Az S3 tárolónak a várt névprefixszel kellett létrejönnie.""" bucket_name = deployed_stack["bucket_name"].value assert bucket_name.startswith("test-bucket"), f"Váratlan tartály név: {bucket_name}" def test_bucket_has_tags(deployed_stack): """Ellenőrizzük, hogy a tároló a szükséges címkékkel jött létre (AWS SDK-n keresztül).""" import boto3 s3 = boto3.client("s3", region_name="us-west-2") bucket_name = deployed_stack["bucket_name"].value tagging = s3.get_bucket_tagging(Bucket=bucket_name) tag_map = {t["Key"]: t["Value"] for t in tagging["TagSet"]} assert tag_map.get("Environment") == "test" assert tag_map.get("ManagedBy") == "pulumi" Futtasd: pytest test_integration.py -v // integration.test.ts import { LocalWorkspace, ConfigValue, UpResult } from "@pulumi/pulumi/automation"; import * as aws from "@pulumi/aws"; import * as pulumi from "@pulumi/pulumi"; import { expect } from "chai"; describe("S3 Bucket Integration", function () { this.timeout(300_000); // 5 perc — a valós telepítések időt vesznek igénybe let outputs: UpResult["outputs"]; let stackName = "integration-test-ts"; before(async function () { const stack = await LocalWorkspace.createOrSelectStack({ stackName, projectName: "bucket-test", program: async () => { const bucket = new aws.s3.Bucket("test-bucket", { tags: { Environment: "test", ManagedBy: "pulumi" }, }); return { bucketName: bucket.id, bucketArn: bucket.arn, }; }, }); await stack.setConfig("aws:region", { value: "us-west-2" }); await stack.workspace.installPlugin("aws", "v7.34.0"); const upResult = await stack.up({ onOutput: console.log }); outputs = upResult.outputs; }); after(async function () { const stack = await LocalWorkspace.selectStack({ stackName, projectName: "bucket-test", program: async () => {} }); await stack.destroy({ onOutput: console.log }); await stack.workspace.removeStack(stackName); }); it("a helyes névprefixszel létrehoz egy tárolót", function () { const name = outputs["bucketName"].value as string; expect(name).to.match(/^test-bucket/); }); }); Futtatás: mocha -r ts-node/register integration.test.ts // integration_test.go package integration_test import ( "context" "fmt" "os" "testing" "github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3" "github.com/pulumi/pulumi/sdk/v3/go/auto" "github.com/pulumi/pulumi/sdk/v3/go/auto/optup" "github.com/pulumi/pulumi/sdk/v3/go/pulumi" "github.com/stretchr/testify/assert" ) func TestBucketIntegration(t *testing.T) { ctx := context.Background() stack, err := auto.NewStackInlineSource(ctx, "integration-test", "bucket-test", func(ctx *pulumi.Context) error { bucket, err := s3.NewBucket(ctx, "test-bucket", &s3.BucketArgs{ Tags: pulumi.StringMap{ "Environment": pulumi.String("test"), "ManagedBy": pulumi.String("pulumi"), }, }) if err != nil { return err } ctx.Export("bucketName", bucket.ID()) return nil }, ) assert.NoError(t, err) _ = stack.SetConfig(ctx, "aws:region", auto.ConfigValue{Value: "us-west-2"}) upResult, err := stack.Up(ctx, optup.ProgressStreams(os.Stdout)) assert.NoError(t, err) t.Cleanup(func() { _, _ = stack.Destroy(ctx) _ = stack.Workspace().RemoveStack(ctx, "integration-test") }) name := fmt.Sprintf("%v", upResult.Outputs["bucketName"].Value) assert.Contains(t, name, "test-bucket") } Futtatás: go test ./... -v -timeout 10m // IntegrationTests.cs using System; using System.Threading.Tasks; using Pulumi.Automation; using Xunit; public class IntegrationTests : IAsyncLifetime { private WorkspaceStack _stack = null!; private UpResult _upResult = null!; public async Task InitializeAsync() { var program = PulumiFn.Create(() => { var bucket = new Pulumi.Aws.S3.Bucket("test-bucket", new() { Tags = new() { ["Environment"] = "test", ["ManagedBy"] = "pulumi" }, }); return new System.Collections.Generic.Dictionary<string, object?> { ["bucketName"] = bucket.Id, }; }); _stack = await LocalWorkspace.CreateOrSelectStackAsync(new InlineProgramArgs( projectName: "bucket-test", stackName: "integration-test", program: program )); await _stack.SetConfigAsync("aws:region", new ConfigValue("us-west-2")); _upResult = await _stack.UpAsync(new UpOptions { OnStandardOutput = Console.WriteLine }); } public async Task DisposeAsync() { await _stack.DestroyAsync(new DestroyOptions { OnStandardOutput = Console.WriteLine }); await _stack.Workspace.RemoveStackAsync("integration-test"); } [Fact] public void BucketNameHasExpectedPrefix() { var name = _upResult.Outputs["bucketName"].Value.ToString(); Assert.StartsWith("test-bucket", name); } } Futtatás: dotnet test // IntegrationTest.java package myproject; import com.pulumi.Context; import com.pulumi.automation.LocalWorkspace; import com.pulumi.automation.UpOptions; import com.pulumi.automation.DestroyOptions; import com.pulumi.automation.ConfigValue; import com.pulumi.automation.WorkspaceStack; import org.junit.jupiter.api.*; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; @TestInstance(TestInstance.Lifecycle.PER_CLASS) class IntegrationTest { private static final String PROJECT_NAME = "bucket_test_project"; private static final String STACK_NAME = "integration-test"; private WorkspaceStack stack; private Map<String, com.pulumi.automation.OutputValue> outputs; @BeforeAll void setUp() throws Exception { stack = LocalWorkspace.createOrSelectStack(PROJECT_NAME, STACK_NAME, (Context ctx) -> { var bucket = new com.pulumi.aws.s3.Bucket("test-bucket", com.pulumi.aws.s3.BucketArgs.builder() .tags(Map.of("Environment", "test", "ManagedBy", "pulumi")) .build()); ctx.export("bucketName", bucket.id()); }); stack.workspace().installPlugin("aws", "v7.34.0"); stack.setConfig("aws:region", new ConfigValue("us-west-2")); var upResult = stack.up(UpOptions.builder().onStandardOutput(System.out::println).build()); outputs = upResult.outputs(); } @AfterAll void tearDown() throws Exception { if (stack != null) { stack.destroy(DestroyOptions.builder().onStandardOutput(System.out::println).build()); stack.workspace().removeStack(STACK_NAME); } } @Test void bucketNameHasExpectedPrefix() { var name = (String) outputs.get("bucketName").value(); assertTrue(name.startsWith("test-bucket"), "Váratlan tartály név: " + name); } } Futtatás: mvn test Használatban lévő Go integrációs keret Ha Go-alapú integrációs teszteket szeretnél, a Pulumi egy célzott Go tesztkeretet szállít a github.com/pulumi/pulumi/pkg/v3/testing/integration címen. Ez kezeli a teljes létrehozás → érvényesítés → megsemmisítés életciklusát bármely Pulumi program számára (bármely nyelven íródott), ExtraRuntimeValidation horgokkal az érvényesített állapot ellenőrzésekhez. func TestS3Website(t *testing.T) { integration.ProgramTest(t, &integration.ProgramTestOptions{ Dir: path.Join("..", "my-s3-website"), Config: map[string]string{"aws:region": "us-west-2"}, ExtraRuntimeValidation: func(t *testing.T, stack integration.RuntimeValidationStackInfo) { url := stack.Outputs["websiteUrl"].(string) resp, _ := http.Get("http://" + url) assert.Equal(t, 200, resp.StatusCode) }, }) } Legjobb gyakorlat: Mindig a teszt veremeket egy finally blokkban vagy teszt lebontási segédprogramban kell megsemmisíteni. A szivárgott integrációs teszt veremek gyakori oka a váratlan felhő költségeknek. Hogyan egészíti ki a politika kód formában az IaC tesztelést? Az egység- és integrációs tesztek érvényesítik, hogy mit csinál az infrastruktúrád. A kód formájában megvalósított politika meghatározza, hogy minek kell megfelelnie az infrastruktúrádnak—biztonsági szabványok, címkézési konvenciók, megfelelőségi követelmények, és automatikusan fut minden pulumi előnézet és pulumi up során. A Pulumi Politikák jelenleg Pythonban és TypeScriptben támogatottak. Politikai írások a verem többi részével azonos nyelven írhatók, anélkül, hogy egy DSL-t kellene megtanulni. A kötelező politika teljesen blokkolja a telepítést, amikor megsértik; egy ajánlási politika figyelmeztetést emel fel, de lehetővé teszi a telepítést. # __main__.py a politikai csomag könyvtáradban from pulumi_policy import ( EnforcementLevel, PolicyPack, ReportViolation, ResourceValidationArgs, ResourceValidationPolicy, ) REQUIRED_TAGS = {"Name", "Environment", "Owner"} def require_tags(args: ResourceValidationArgs, report_violation: ReportViolation): if args.resource_type.startswith("aws:"): tags = args.props.get("tags") or {} missing = REQUIRED_TAGS - set(tags.keys()) if missing: report_violation( f"Az erőforrásnak hiányzik a következő kötelező címkéje: {', '.join(sorted(missing))}" ) def no_public_s3(args: ResourceValidationArgs, report_violation: ReportViolation): if args.resource_type == "aws:s3/bucket:Bucket": if args.props.get("acl") in ("public-read", "public-read-write"): report_violation("Az S3 tárolóknak nem szabad nyilvános ACL-t alkalmazniuk.") PolicyPack( name="aws-security-baseline", policies=[ ResourceValidationPolicy( name="require-tags", description="Minden AWS erőforrásnak rendelkeznie kell a Nevével, Környezetével és Tulajdonosával címkékkel.", enforcement_level=EnforcementLevel.MANDATORY, validate=require_tags, ), ResourceValidationPolicy( name="no-public-s3", description="Az S3 tárolóknak nem lehetnek nyilvánosan hozzáférhetők ACL-en keresztül.", enforcement_level=EnforcementLevel.MANDATORY, validate=no_public_s3, ), ], ) // index.ts a politikai csomag könyvtáradban import * as aws from "@pulumi/aws"; import { PolicyPack, validateResourceOfType } from "@pulumi/policy"; new PolicyPack("aws-security-baseline", { policies: [ { name: "require-tags", description: "Minden EC2 példánynak rendelkeznie kell Nevével, Környezetével és Tulajdonosával címkékkel.", enforcementLevel: "mandatory", validateResource: validateResourceOfType(aws.ec2.Instance, (instance, args, reportViolation) => { const required = ["Name", "Environment", "Owner"]; const tags = instance.tags || {}; for (const tag of required) { if (!tags[tag]) { reportViolation(`Hiányzó kötelező címke: ${tag}`); } } }), }, { name: "no-public-s3", description: "Az S3 tárolóknak nem szabad nyilvános ACL-t alkalmazniuk.", enforcementLevel: "mandatory", validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => { if (bucket.acl === "public-read" || bucket.acl === "public-read-write") { reportViolation("Az S3 tárolónak nem szabad nyilvános ACL-t alkalmaznia."); } }), }, ], }); Politikai pak megfutása # Új politikai csomag létrehozása egy sablonból mkdir my-policies && cd my-policies pulumi policy new aws-typescript # Futtasd a politika ellenőrzéseket egy verem ellen pulumi preview --policy-pack ./my-policies pulumi up --policy-pack ./my-policies # Vagy publikáld a Pulumi Cloudban és érvényesítsd a szervezet szintjén pulumi policy publish Ahogy Joe Duffy, a Pulumi vezérigazgatója mondja: "A világ legintelligensebb ügynökének is szüksége van védőkorlátokra, ellenőrző nyomokra és politikai végrehajtásra ahhoz, hogy megbízható legyen a termelési rendszereknél nagy méretekben." A politikai ellenőrzések kiegészítik az egységteszteket: egyes tesztek ellenőrzik az érvényességet ("címkéket írtam be ebbe az erőforrásba"), míg a politikák érvényesítik a szabványokat ("minden erőforrásnak rendelkeznie kell ezekkel a címkékkel, függetlenül attól, hogy mit akart egy-egy fejlesztő"). A bonyolult megfelelőségi környezetekben publikáld a politikákat a Pulumi Cloudban, és rendeld hozzá őket politikai csoportokhoz, hogy érvényesítsd őket az általad választott veremeken vagy fiókokban—beleértve a vállalati szintű érvényesítést is. Hogyan különbözik a Pulumi IaC tesztelése a Terraform-tól? Az alapvető különbség a nyelvi kohézió. A Pulumi esetében az infrastruktúrát, a teszteket és a politikákat ugyanabban a általános célú nyelven írod. A Terraform esetében az infrastruktúrád HCL, de a teszteid másnak kell lenniük—vagy Go-nak (Terratest) vagy egy korlátozott HCL DSL-nek (terraform test). Pulumi Terratest terraform test (≥ 1.6) A tesztek nyelve Ugyanaz a nyelv, mint az infra (Python, TypeScript, Go, C#, Java); YAML programokat tesztelnek az Automatizációs API-n keresztül bármelyik nyelvről Mindig Go (függetlenül az infra nyelvétől) HCL DSL (.tftest.hcl) Egységteszt hamisítványokkal Igen — pulumi.runtime.set_mocks(), nem szükséges felhői hitelesítő adatok Nem — nincs hamisítvány réteg a felhőszolgáltatók számára Korlátozott — mock_provider (v1.7+) HCL-deklaratív, nincs programozásbeli logika Integrációs tesztelés Automatizációs API (bármilyen nyelv) vagy Go keret Teljes funkcionalitású, de csak Go-ra Igen, az alkalmazás = teljessé teszi a futtatás és a végrehajtás blokkokat Politika / védőkorlátok Pulumi Politikák: Python vagy TypeScript, futás előnézeti időszakban Külső eszközök (checkov, tfsec, OPA/Rego - külön eszközkészlet) Sentinel (vállalati verzió) vagy külső Végrehajtási sebesség Egység: milliszekundumok; integráció: attól függ, hogy a felhő Percek (valódi telepítések alapértelmezés szerint) Csak tervezés: gyors; alkalmazás: percek Tanulási görbe Használj meglévő nyelvi készségeket és tesztkereteket Megköveteli a Go-ismereteket és a go test megértését Alacsony, ha ismered az HCL-t; korlátozott kifejezési lehetőségek A Terratest bevált megoldás és széles körben elfogadott, különösen a Terraform modul tesztelésében. Mély integrációt kínál az AWS, GCP, Azure és Kubernetes szolgáltatásokhoz. Korlátozása azonban, hogy nincs hamisítvány réteg a felhőszolgáltatók számára, így a legtöbb tesztcsomag valós infrastruktúrát telepít, ami lassabbra és költségesebbé válik minden egyes kötelező végrehajtásnál. A terraform test jelentős javulásokon ment keresztül a Terraform 1.6 és 1.7 verzióiban. A parancs = terv üzemmód elkerüli a valós telepítéseket, és a mock_provider alapvető egységszerű tesztelést tesz lehetővé. De a kifejezési lehetőségek a HCL deklaratív modell által korlátozottak—nincsen általános célú programozás, nincsenek harmadik fél által külön létrehozott állítási könyvtárak, és egy rögzített futtatási/ellenőrzési tesztkonstrukciós készlet áll rendelkezésre. A Pulumi esetében nincs kontextusváltás. Ugyanaz a fejlesztő, aki megírja az infrastruktúrát, ugyanabban a nyelven írja a teszteket, ugyanazzal az IDE-vel, ugyanazzal a debuggerral és ugyanazzal a CI csővezetékkel. Ez a kohézió a termelékenység szorzója. Hogyan állítsuk be IaC tesztelését Pulumi-val Kövesd ezeket a lépéseket, hogy teljes tesztkészletet adj hozzá egy Pulumi projekthez. A példák Python-t használnak, de a minta azonos TypeScript, Go, C#, vagy Java esetében. 1. lépés: Telepítsd a Pulumi-t és válassz egy nyelvi SDK-t # Telepítsd a Pulumi CLI-t curl -fsSL https://get.pulumi.com | sh # Hozz létre egy új projektet (vagy használj egy meglévőt) pulumi new aws-python --name my-project --stack dev 2. lépés: Adj hozzá egy tesztkeretet # Python pip install pytest pytest-asyncio # TypeScript npm install --save-dev mocha ts-node @types/mocha chai @types/chai # A Go az alapértelmezett tesztcsomagot használja—nincs további függőség 3. lépés: Készítsd el a Mocks osztályt Hozz létre mocks.py-t (vagy hasonlót) a Mocks alosztályoddal. Tartsd egyszerűnek az egységtesztekhez: add meg az [args.name + "_id", args.inputs] értékpárt a new_resource-ból. 4. lépés: Írd meg az egységteszteket Hozz létre test_infra.py-t a projekt gyökerében. Importáld először a hamisítványokat, majd a programodat. Írj tesztfüggvényeket, amelyek a @pulumi.runtime.test és pulumi.Output.all(...).apply(...) használatával érvényesítik az erőforrás tulajdonságait. 5. lépés: Adj hozzá integrációs teszteket Hozz létre test_integration.py-t egy pytest segédprogrammal, amely létrehoz egy veremet, meghívja a stack.up()-ot, visszaadja az eredményeket az érvényesítésekhez, és a stack.destroy()-t lebontja. Irányítsd ezt egy dedikált tesztfelhő fiók felé. 6. lépés: Készíts egy politikai csomagot mkdir policies && cd policies pulumi policy new aws-python # Szerkeszd a __main__.py-t a szabályaid hozzáadásához 7. lépés: Kösd össze az egészet a CI-vel # .github/workflows/test.yml (GitHub Actions kivonat) jobs: unit-tests: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pulumi/actions@v6 - run: pip install -r requirements.txt - run: python -m pytest tests/unit/ -v integration-tests: runs-on: ubuntu-latest needs: unit-tests environment: staging steps: - uses: actions/checkout@v4 - uses: pulumi/actions@v6 - run: pip install -r requirements.txt - env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} run: python -m pytest tests/integration/ -v --timeout=300 Gyakran ismételt kérdések Mi az infrastruktúra mint kód tesztelés? Az infrastruktúra mint kód tesztelés az automatizált ellenőrzések írásának gyakorlata, amelyek érvényesítik az IaC programjaidat, mielőtt azok a termelésbe kerülnének. Pontosan úgy, mint az alkalmazás tesztelésénél, ez tartalmaz egységteszteket (gyors, nincs felhői hitelesítési adatok, hamisítják a szolgáltatót), integrációs teszteket (valódi felhőtelepítések ephemerális környezetben) és politikai ellenőrzéseket (szabályok, amelyek érvényesítik a megfelelőségi szabványokat minden előnézet és telepítés során). Tesztelhetsz-e IaC-t a felhő telepítése nélkül? Igen. A Pulumi hamisítvány kerete interceptálja az összes felhő API-hívást, és bemeneti stub-okkal helyettesíti őket. Az egységtesztek teljesen memóriában futnak—nincs felhői hitelesítés, nincs hálózati hívás, nincs költség. Egy 50 egységtesztből álló csomag kevesebb mint egy másodperc alatt lefut. Ez az elsődleges oka annak, hogy érdemes egységteszteket írni: gyors, ingyenes, determinisztikus visszajelzés minden kötelező végrehajtásnál. Szükséged van valós felhői hitelesítő adatokra a Pulumi teszteléséhez? Csak az integrációs tesztekhez. Az egységtesztek használják a Pulumi hamisítvány futásidejét, és nem igényelnek felhői hitelesítő adatokat. A politikai ellenőrzések is futnak hitelesítési adatok nélkül az előnézeti időszakban. Az Automatizációs API-t használó integrációs teszteknek viszont szükségük van hitelesítő adatokra—valódi erőforrásokat telepítenek—tehát jellemzően egy dedikált tesztfiókkal rendelkező CI szakaszban futnak, nem minden PR-n. Mi a különbség az egység- és integrációs tesztek között az IaC számára? Az egységtesztek a hamisított felhőszolgáltatók használatával érvényesítik az erőforrás tulajdonságait, konfigurációit és kapcsolatait—milliszekundumok alatt futnak, nincs szükség felhői hitelesítő adatokra. Az integrációs tesztek valós erőforrásokat telepítenek egy valós felhőkörnyezetbe, érvényesítik a futásbeli viselkedést (válaszol-e az végpont? fogadja-e a kapcsolatokat az adatbázis?) és mindent megsemmisítenek. Az egységtesztek olcsók és gyakoriak; az integrációs tesztek lassabbak, valódi felhőköltséget igényelnek, és ritkábban futnak. Hogyan hamisíthatók cloud erőforrások a Pulumi-ban? Implementáld a pulumi.runtime.Mocks-ot (Python) vagy használd a pulumi.runtime.setMocks()-t (TypeScript/JavaScript) a Pulumi program importálása előtt. A new_resource metódus minden erőforrás regisztrációt fogad, és visszaad egy hamis [id, állapot] párt. A call metódus kezeli az adatforrást kereső lekérdezéseket. Állítsd be a hamisítványokat a pulumi.runtime.set_mocks(MyMocks())-re, majd importáld a programodat—a futásidő minden erőforrás-regisztrációt végigirányít a hamisítványodhoz a valós API-hívások helyett. Milyen tesztkeretek működnek a Pulumi-val? Bármilyen nyelv natív tesztkerete működik. Python esetén: pytest (ajánlott) vagy unittest. TypeScript/JavaScript esetén: Mocha, Jest vagy Vitest. Go esetén: a szabványos tesztcsomag. C# esetén: NUnit vagy xUnit. Java esetén: JUnit. Az @pulumi/pulumi/automation SDK és a pulumi.automation modul természetesen integrálódik mindegyikkel. A YAML a Pulumi deklaratív konfigurációs nyelve; mert nincs vezérlési áramlata vagy végrehajtható nyilatkozatai, nem írhatsz tesztlogikát magában a YAML-ban. A YAML programokat a Pulumi Automatizációs API-ra mutatva tesztelheted Python, TypeScript, Go, C#, vagy Java segítségével, és érvényesítheted a verem kimeneteit. Hogyan tesztelheted a Pulumi-t CI/CD-ben? Oszd a tesztkészletedet két CI szakaszra. 1. szakasz (egységtesztek): minden PR kötelező végrehajtáskor fut, nincs szükség felhői hitelesítő adatokra, gyors. 2. szakasz (integrációs tesztek): futása a főágba való egyesítéskor vagy ütemezéskor, szükséged van felhői hitelesítő adatokra egy dedikált tesztfiókhoz, az Automatizációs API-t használja ideiglenes veremek telepítésére. A Pulumi GitHub Actions integráció egyszerűsíti a hitelesítő adatok befecskendezését és a verem kezelést mindkét szakaszban. A politikai kód formája ugyanaz, mint a tesztelés? A politikai kód formája kiegészíti, de különbözik a teszteléstől. A tesztek érvényesítik, hogy mit produkál a programod (az erőforrás A-nak van B tulajdonsága); a politikák érvényesítik azt, hogy minek kell megfelelnie az erőforrásaidnak (minden erőforrásnak rendelkeznie kell X címkével). A politikák automatikusan futnak az előnézeti időszakban, és blokkolják a nem megfeleltetett telepítéseket a telepítési időszakban. Együtt védelmet nyújtanak a mélységetől: a tesztek az érvényességet igazolják, a politikák pedig érvényesítik a normákat függetlenül az érvényesítési szándéktól. Hogyan más a Pulumi tesztelése a Terratest-től? A Terratest jellemzően valós infrastruktúrát telepít—nincs hamisítvány réteg a felhőszolgáltatók számára—így a legtöbb tesztfuttatás valódi felhő erőforrásokat biztosít, ami tényleges költségeket és 5–30 percet vesz igénybe. A teszteket mindig Go nyelven kell írni, függetlenül attól, hogy milyen nyelvet használ az infrastruktúrád. A Pulumi egységteszt kerete milliszekundumok alatt fut, nincsenek felhői hitelesítő adatok, a meglévő nyelves és tesztrunner-t használja, és valós hamisítvány-izolációt biztosít. Az integrációs teszteléshez a Pulumi Automatizációs API egy hasonló modellt kínál Terratest különböző nyelveken. Hogyan lehet lebontani az erőforrásokat integrációs tesztek után? Python pytest esetén használj egy modul-szoros segédprogramot, amely átadja a stack.up()-ot, és a teardown-ban végrehajtja a stack.destroy()-t. TypeScript Mocha esetén hívd meg a stack.destroy()-t egy after() horgon. Go esetén használd a defer stack.Destroy(ctx, ...). Mindig használj try/finally mintát (vagy annak megfelelőjét a nyelveden), hogy a teardown még akkor is fusson, ha a teszt állítása megbukik. A Pulumi Cloud rendelkezik beépített verem TTL és auto-destroy funkciókkal a további biztonság érdekében. Hová lehet menni következő lépésként? Az IaC tesztelése nem egy mindent vagy semmit befektetés. Kezdj az egységtesztelés közvetlen hozzáadásával a legkritikusabb veremhez—arra, ami a termelési VPC-det, az IAM szerepeidet vagy az adat-infrastruktúrádat kezeli. Válassz ki három-öt tulajdonságot, amelyek esetén, ha hibásak, kiesést vagy biztonsági incidenst okoznának. Írj teszteket ezekre. Futtasd ezeket a CI-ben. Terjeszkedj innen. Amikor készen állsz a mélyebb mélységre: Tesztelési áttekintés—Pulumi teljes tesztelési dokumentációja Egységteszt kézikönyv—részletes hamisítvány API referencia Go és C# példákkal Automatizációs API integrációs tesztelés—haladó minták (multi-stack tesztek, konfigurációs injekció, párhuzamos tesztvégrehajtás) Politika mint kód szerzői—Pulumi politikák írása és publikálása Pulumi vs. Terraform—a két platform teljes összehasonlítása Ha a Terraform migrációját végzed, a Pulumi átkonvertáló eszköz átkonvertálja a meglévő HCL-edet Python, TypeScript, Go, C#, és egyéb nyelvekre, beleértve a tesztinfrastruktúrádat. A Terratest vagy terraform teszt csomagjaid átültethetők a Pulumi natív tesztfuttatójába—ugyanazzal a nyelvvel—részét képezve a migrációnak. Kezdd el a Pulumi-t ingyen →
Külső link: pulumi.com
Kapcsolódó cikkek
devops
Hét stabil kernel szombatra, köztük két biztonsági javítással.
devops
Miért nem mentik meg az olcsóbb modellek az AI költségvetését?
devops
