"Pizza as a Service" og testing av "Infrastructure as Code"

Av Richard Rostad, Promis Qualify

La oss først snakke litt om Pizza:

Hvis vi lager en hjemmelaget pizza må vi holde styr på hele prosessen og er ansvarlige for alt fra ingredienser til at komfyren har strøm og helt frem til våre gode venner er gode og mette. Alternativt kan vi kjøpe en Grandiosa. Det blir ikke nødvendigvis det samme restultatet, men på den annen side trenger vi ikke tenke på ingredienser og sammensetning. Ovn, drikkevarer og gode venner må vi fremdeles sørge for.

Vi kan også bestille ferdig varm pizza levert på døren. Det blir ganske mye enklere og vi trenger bare å tenke på et sted å spise, god drikke og gode venner.

Hvis også det blir for mye å holde styr på kan vi be våre gode venner ut på en pizzarestaurant der utvalget av venner er det eneste vi trenger å tenke på. Dette kan vi fremstille slik:

 

Med skytjenester blir det nesten på samme måten: Vi kan ha det hele i kjelleren der vi er ansvarlige for at alt virker, vi kan bestille “Infrastructure As A Service” — der vi ikke trenger å holde styr på selve maskinparken, “Platform As A Service” — der vi egentlig bare har ansvar for våre egne applikasjoner og brukere eller "Software As A Service" der vi bare trenger å tenke på brukerne. De fleste kjenner den siste modellen i form av Office365.

 

For oss testere har dette både fordeler og ulemper; ting blir enklere fordi vi ikke har problemer med falske positive fordi noen har snublet i en nettverkskabel i kjelleren. På den annen side har vi en utfordring med ting som ytelsestester: Det eneste vi får testet med å kjøre en ytelsestest med stor belastning mot en Amazon PaaS er hvor stor regning Amazon kan sende oss — fordi plattformen skalerer sømløst uten vår innvirkning. Det er som regel en lite matnyttig test. Imidlertid er det fremdeles ønskelig å kjøre ytelsestester slik at vi kan avdekke flaskehalser for nettopp å redusere regningen fra Amazon, men de må kjøres på våre egne applikasjoner i isolasjon. Vi taper ikke så mye på det; all erfaring tilsier at flaskehalser, ineffektiv kode og bortkastede CPU sykluser ligger i den koden vi skriver selv — ikke den koden vi leier av Amazon. Når vi sier at vi ikke trenger å tenke på infrastruktur, er det som så mye annet en sannhet med modifikasjoner. Riktignok er leverandøren i de interessante scenariene ansvarlig for infrastruktur, men vi er ansvarlige for konfigurasjonen av infrastrukturen. Siden denne infrastrukturen styres av en skyplattform er det ikke lenger kabler som er plugget i riktig switch i kjelleren, men “Infrastructure as Code”

IaC er akkurat det det høres ut som; kode som styrer infrastrukturen vår. Som all annen kode bør også infrastrukturkode testes. Det er en ny og spennende problemstilling som ikke fryktelig mange testere har erfaring med enda.

 

Testing av infrastrukturkode

Infrastrukturkode har en del egenskaper som gjør at den normalt testes med andre metoder enn vanlig kode. Den viktigste forskjellen er at infrastrukturkode i all hovedsak er deklarativ hvilket vil si at den ikke er bygget opp rundt metoder og klasser men påstander om hvordan man ønsker verden skal se ut.

Eksempelvis: 

logstash:
  build:
     context: logstash/
     args:
        ELASTIC_VERSION: ${ELASTIC_VERSION}
     volumes:
- ./logstash/config/logstash.yml:/usr/share/ logstash/config/logstash.yml:ro,Z
- ./logstash/pipeline:/usr/share/logstash/ pipeline:ro,Z
     ports:
       - "5044:5044" - "5007:5000/tcp"
       - "5007:5000/udp"
       - "9600:9600"
     environment:
        LS_JAVA_OPTS: -Xms256m -Xmx256m
        LOGSTASH_INTERNAL_PASSWORD: $ {LOGSTASH_INTERNAL_PASSWORD:-}
     networks:
       - elk
     depends_on:
       - elasticsearch

For folk flest ser ikke dette umiddelbart ut som testbar kode; den gjør jo ingen ting! Men vi gir oss ikke så lett; all kode er testbar på ett eller annet vis. Det vanligste testforløpet for infrastrukturkode ser ut omtrent slik:

 

Først kjører vi noen statisk analyseverktøy og sjekker at dette faktisk er gyldig kode. Gjerne utvidet med en avsjekk av at skyplattformen vi ønsker å kjøre på faktisk støtter de parameterne og grensesnittene vi har definert (uten/m API validering). Så kan vi teste mot et mock API for å sjekke at innholdet i strengene vi sender inn også er gyldige.

Step 2 er å sjekke hvilke avhengigheter vi har og om de har noen fallgruver vi ikke har tenkt på. Hva om port 5044 i eksemplet over allerede er i bruk av en annen tjeneste vi har konfigurert? Vil vi oppdage det under test eller venter vi til en produksjonslogg fylles opp med “400: Bad Request” ?

Til slutt kjører vi noen enkle tester direkte mot plattformen i forhåndsvisningmodus, sjekker at ting faktisk dukker opp som planlagt og gjør til slutt en rask utforskende test av om resultatet faktisk er brukbart.

Mange av oss er vant til at tester er fordelt i en pyramideform; vi har mange enhetstester som understøtter noen færre integrasjonstester som kjøres før vi foretar noen få utforskende sesjoner for å finne de ‘dype’ feilene. Siden infrastrukturkode ikke har noen ‘enheter’ som sådan, og det aller meste av kompleksitet handler om avhengigheter på både kryss og tvers får testvolumet vårt en ganske annen fasong, kanskje som:

 

En annen ting er at tilsynelatende enkle tester kan ta fryktelig lang tid; en test som tester

   depends_on:
          - elasticsearch

vil faktisk installere og starte opp en komplett instans av elasticsearch. Det kan ta tid.

Derfor er det ikke uvanlig å ‘teste progressivt’ vet at man prioriterer testene etter hvor lang tid man forventer at testen kommer til å ta, ikke som normalt prioritert etter risiko.

Det betyr at man kjører de sjekkene som tar minst tid først og som sjekker at grunnleggende infrastruktur er på plass. Når disse har passert vet man at man har et grunnlag for å teste ‘dypere’ med mer kompliserte og tidkrevende tester.

En annen måte å teste progressivt på, eksempelvis dersom man ikke har erfaringstall som tilsier hvilket tidsforbruk man kan forvente er i økende grad av kompleksitet:

 

Først tester man internt i en container. Så tester man noen få avhengigheter på en nedskalert og/eller lokal plattform (Docker compose, minikube, podman e.l) før man tester på et ‘fullskala’ miljø med alle relevante avhengigheter. Normalt bør man tenke seg godt og lenge om før man gjentar en test — fordi en gjentatt test bare demonstrerer at det ikke har skjedd endringer siden sist — slik at det normalt er bedre med en ny og variert test enn repetisjon av en gammel. Ved denne formen for progressiv test av deklarativ kode er det dog ofte nyttig å repetere utvalgte tester fordi man vet at alt utenom testobjektet har endret seg siden forrige kjøring.

Som du kanskje kan gjette deg til har vi normalt høyere automatiseringsgrad på infrastrukturtester enn annen type testing. Mens vi snakker om automatisering; en vanlig måte å spesifisere en test på er Given-When-Then, som eksempelvis i verktøy som Cucumber:

given User{
     it { exists }
     it { has no subscription }
     }

Det blir fort litt snodig med deklarativ kode siden den ikke inneholder forretningslogikk. En typisk test vil da bare inneholde ‘Given’ biten:

when requests(discount) {}
then discount(value: “10%”)
given virtual_machine(name: "appserver-testcustomerAstaging") {
   it { exists }
   it { is_running }
   it { passes_healthcheck }
   it { has_attached storage_volume(name: "app-storage-testcustomerA-staging") }
}

For noen av oss som er vant til at testing er en aktivitet som faktisk utfører en handling er dette en ny og spennende måte å tenke på, dog forbausende nyttig.