Blog

Så tillämpar du DevOps-metoder för Infrastructure as Code

DEC 17, 2021

Att utveckla och underhålla infrastruktur kan vara en skrämmande upplevelse som Ops- och SRE-team kanske drar sig för – ”Rör inte ett system som fungerar”. Men det behöver inte vara så!

Michael Vittrup Larsen

Michael is a Consultant at our office in Aarhus. He has an MScEE from Aalborg University where he specialized in digital signal processing. Before joining us Michael was working on building and optimizing cloud infrastructure for the telecoms sector. In his spare time, he likes to explore the wilderness on his mountain bike.

Det finns tre centrala DevOps-metoder som påverkar vår mjukvaruleverans och operativa prestanda (läsningstips: ”Infrastructure as Code” av Kief Morris):

  • Definiera allt som kod

  • Testa och leverera kontinuerligt allt pågående arbete

  • Bygg små, enkla delar som du kan ändra oberoende av varandra

Om du arbetar med modern mjukvaruutveckling är detta ingen nyhet för dig. Men visste du att samma principer även gäller för hur vi utvecklar modern infrastruktur?

I det här blogginlägget får du lära dig hur du kontinuerligt testar infrastruktur, alltså Continuous Integration.

Continuous Integration för Infrastructure as Code

Mjukvaruutvecklare har vant sig vid snabba cykler utan att kompromissa med kvalitet och stabilitet. Det har möjliggjorts genom automatiserad testning av alla kodändringar när de checkas in i Git. Ändringar som inte klarar de automatiserade testerna får inte mergas in i vår kodbas. Automatiserad testning är skyddsräckena som möjliggör snabb mjukvaruutveckling.

Samma metoder för Continuous Integration kan tillämpas på infrastruktur. Vi kan definiera policyer för egenskaper hos vår infrastruktur. Dessa policyer kan valideras genom tester, vilket ger oss den kombination av hastighet, stabilitet och kvalitet som vi har vant oss vid inom mjukvaruutveckling.

Infrastruktur handlar inte bara om servrar

I det här sammanhanget avser vi med ”infrastruktur” allt som vi kan hantera programmatiskt med ett arbetssätt där infrastruktur definieras som data. Det omfattar till exempel:

  • servrar

  • nätverk

  • användarautentisering och auktorisering

  • driftsättning av applikationer

  • pipeline-konfiguration

Några typiska exempel är allt vi kan hantera med Terraform samt alla Kubernetes-resurstyper.

Infrastructure as Code jämfört med infrastruktur som data

Begreppen ”Infrastructure as Code” och ”infrastruktur som data” används ofta synonymt, men de betyder olika saker. Även ”deklarativ infrastruktur” används ofta för att beskriva det vi här kallar ”infrastruktur som data”. Men den viktiga poängen är:

Vi kan göra påståenden om infrastruktur som definieras som data. Det är betydligt svårare med Infrastructure as Code.

Följande kommando skapar en VM-instans i AWS-molnet med hjälp av AWS CLI. En specifik maskinavbildning (en ”AMI – Amazon Machine Image”) och maskinkonfiguration (”instanstyp”) anges.

Det här kommandot kan placeras i ett skript, och då har vi ”Infrastructure as Code”. Föreställ dig dock att vi vill att skriptet ska vara idempotent, det vill säga att det bara skapas en VM även om skriptet körs två gånger. Det skulle kunna lösas med skriptlogik, men skriptet kan till slut bli ganska komplicerat och felbenäget.

Ett alternativ är att använda till exempel Terraform, där vi kan definiera följande ”datastruktur” som beskriver det önskade tillståndet för vår infrastruktur. Denna ”infrastruktur som data” har fördelen att Terraform hanterar all logik som behövs för att förena vårt önskade tillstånd med infrastrukturens faktiska tillstånd. Kubernetes fungerar på liknande sätt, men med andra ”datastrukturer” (även kallade ”Kubernetes resource YAML”).

Utöver att exempelvis Terraform eller Kubernetes hanterar en del av komplexiteten i infrastrukturhanteringen finns ytterligare en fördel i hur infrastrukturen definieras. Med Infrastructure as Code är det generellt mycket svårt att bedöma kodens effekt och korrekthet. Ett verktyg som gör en sådan verifiering behöver dessutom stödja flera språk och externa verktyg.

Tänk dig till exempel shellskript som använder en kombination av AWS CLI, jq, sed, awk, grep och så vidare. Med datastrukturmetoden blir det mycket enklare, eftersom datastrukturen vanligtvis har ett relativt enkelt format och endast schemat för de enskilda objekten i datastrukturen är domänspecifikt, till exempel AWS Terraform-resurser eller Kubernetes-resurser.

Därför fungerar verktyg som Open Policy Agent på data, och användningsområdena omfattar infrastruktur som data.

Open Policy Agent och relaterade verktyg

Nu ska vi titta på hur du kan använda dessa verktyg för att implementera policyer för vår infrastruktur som data:

Open Policy Agent (OPA) är grundverktyget som de följande verktygen bygger på. OPA är en policy motor som gör att vi kan definiera policyer som kod, som vår infrastruktur kan testas mot. Policyer definieras i ett språk som kallas Rego.

Conftest är ett verktyg som integrerar OPA och tar emot strukturerad data i flera olika format, till exempel YAML och JSON. Data kan exempelvis vara Kubernetes-resurser i YAML-format eller en Terraform-plan i JSON-format, och Conftest låter oss utvärdera Rego-policyer mot dessa data. Conftest är idealiskt för att validera infrastruktur som en del av en CI-pipeline.

Regula är ett Rego-bibliotek som kan användas tillsammans med Conftest och som är särskilt utformat för Terraform-infrastrukturdata.

GateKeeper är en integration av OPA med Kubernetes. GateKeeper är en Kubernetes admission controller som styr åtgärder på Kubernetes-resurser via Kubernetes API. Den kan till exempel tillåta eller neka skapandet av en POD baserat på Rego-policyer som OPA utvärderar mot POD-definitionen. GateKeeper används alltså inte under CI-processen (där använder vi Conftest), men bör användas med samma policyer som Conftest för att styra vad som faktiskt driftsätts på Kubernetes.

Språket Rego

Rego är ett språk för datafrågor, och kärnan i språket är konceptet regler, som består av frågor som bekräftar ett visst tillstånd i de data som frågas ut.

Låt oss titta på ett enkelt exempel med följande dataset:

Vi kan fråga detta dataset med Rego-regler och skapa nya dataset utifrån en uppsättning villkor. I den bemärkelsen liknar Rego-regler databasfrågor. Följande regel skapar ett dataset med alla mänskliga varelser från datasetet:

Kodraderna i denna regel ska läsas så här:

  1. Regeln heter ”humans”, tar emot indata ”name” och returnerar ett dataset baserat på tilldelningar av ”creature” i regelns kropp.

  2. Tilldela ”creature” valfritt värde från datasetet ”creatures” – understrecksvariabeln betyder ”loopvariabel vars värde vi inte bryr oss om”. Inom denna regel får ”creature” alla fyra värden från listan ”creatures”, vilket alltså påminner om en for-loop.

  3. Kontrollera att ”creature” är en människa. Om något villkor utvärderas till false kommer det resulterande datasetet inte att innehålla det specifika värdet för ”creature”.

  4. Tilldela name från den specifika ”creature”.

Observera att vi inte angav något värde för indata ”name”, vilket innebär att denna variabel inte är bunden till ett specifikt värde och att reglerna därför löses utan begränsning av värdet för ”name”. Hade vi angett ett värde skulle vårt genererade dataset begränsas därefter:

I Rego betyder likhetstecknet ”=” både jämförelse och tilldelning, och det kan tilldela både från vänster till höger och från höger till vänster. Det bör därför ses som en matematisk ekvation. Följande rad från regeln ovan var en tilldelning vid den första regelkörningen, eftersom vi inte angav ett värde för ”name”, och en jämförelse vid den senare regelkörningen.

Ordningen på satserna, och även ordningen på elementen på vänster och höger sida om likhetstecknet ”=”, spelar ingen roll. Därför är följande regeldefinition identisk med den ovan:

Vi kan slå samman våra två dataset med en regel som följande:

I denna regel sker sammanslagningen av datasetet favorite_food och listan över vad varje varelse gillar på sista raden. Observera återigen understrecket som används för att indexera i listan ”likes” – vi bryr oss inte om det faktiska indexet i detta fall.

Ett vanligt sätt att arbeta med Rego är att bygga grundläggande regler som sedan kombineras till mer avancerade regler. Följande regel kombinerar de två föregående reglerna för att returnera en lista över människor som gillar maten i favoritlistan:

Om vi kör denna regel får vi:

Exempel – taggningspolicy för AWS

Taggning är en mycket användbar teknik vid driftsättning av resurser på AWS molnplattform. Föreställ dig att vi har en företagspolicy som säger att alla resurser ska taggas med taggen ”Owner”, som anger vem som ansvarar för en viss resurs. Hur kan vi göra det med Rego?

Följande policy (anpassad från projektexempel i Regula) implementerar detta taggningskrav med Conftest och Regula.

Först definierar vi en lista över AWS-resurstyper som stöder taggning (listan är förkortad för tydlighetens skull) och en lista över taggar som vi kräver för våra resurser. aws_instance är VM-resurstypen som vi såg ovan, och följande, som definierar två mängder, ser inte särskilt annorlunda ut än i andra språk.

Därefter definierar vi en Rego-regel som kopplar samman vår lista över AWS-resurser med listan över AWS-resurstyper som kan taggas. Detta påminner mycket om vårt exempel med regeln ”humans” ovan:

Därefter definierar vi en Rego-funktion som, givet en resurs, jämför taggarna på resursen med vår lista över obligatoriska taggar.

Slutligen definierar vi policyn. Baserat på resultatet från Rego-regeln/-funktionen ovan avgör vi om vi ska tillåta eller neka en viss resurs:

Exempel – policy för Kubernetes Service-typ

Om ditt team driftsätter flera tjänster i Kubernetes kanske ni vill hantera lastbalanserare och TLS-certifikat centralt (till exempel med en routningsarkitektur i flera lager). I en sådan situation vill vi säkerställa att teamen inte driftsätter Kubernetes-tjänster av typen ”LoadBalancer”. Hur skulle en Rego-policy för detta kunna se ut?

Följande policy implementerar detta krav för Kubernetes-tjänsttyp med ”input” som källa för den Kubernetes-resurs som utvärderas.

Du kanske nu funderar: ”Varför inte bara använda Kubernetes RBAC för att begränsa vad som kan driftsättas?”

Skillnaden är att vi med Rego-policyer validerar vår infrastrukturspecifikation före driftsättning, till skillnad från att tillämpa en policy när infrastrukturen redan har driftsatts. Det liknar hur vi kör tester som en del av CI-pipelines för att fånga upp fel före driftsättning.

Tillämpa policyer med GateKeeper

De två föregående policyexemplen visar hur du kan validera infrastrukturresurser, till exempel som en del av en CI-pipeline, innan ändringar godkänns för driftsättning. Rego-policyer kan på samma sätt tillämpas i Kubernetes genom Kubernetes admission controller GateKeeper. GateKeeper-policyer implementeras i Rego och konfigureras i Kubernetes genom anpassade resursdefinitioner. Se till exempel Kubernetes Gatekeeper Service type template för en policy som är mycket lik den ovan för Kubernetes-tjänster av typen LoadBalancer.

Vanliga saker som dina policyer kan kontrollera:

  • Förbjud hostPath-volymer

  • Att resursgränser och resursbegäranden för containrar är angivna (och har rimliga värden)

  • Att containerbilder hämtas från specifika register, och eventuellt även att containertaggar är digestar och inte föränderliga taggar (som ”1.0”)

  • Förbjud vissa intervall för node ports

  • Obligatoriska och/eller unika taggar

  • Att Ingress-sökvägar är giltiga och/eller unika

  • Att POD:ar har readiness- och liveness-prober

  • Att containrar inte körs som root

GateKeeper-policytemplates kan konfigureras med parametrar, så att olika policyer kan konfigureras för olika namespaces. Se till exempel GateKeeper-policyn ContainerLimits.

Begränsningar med deklarativa tester

När infrastruktur definieras med en deklarativ ”som data”-metod finns det en gräns för hur mycket testning som är meningsfull. Testningen går snabbt över till att vi testar våra egna deklarativa påståenden, som följande:

Deklarativ testning är mest användbar för exempelvis policyer och gränssnitt mellan team. Här ansvarar olika parter för tilldelningen och testet ovan.

De ”data assertions” som visas i det här blogginlägget kan också bara göra assertioner baserat på tillgängliga data. Komplexa system kan ha data på många olika platser och i olika format, ofta med separerad dataåtkomst av säkerhetsskäl. Det gör det svårt, eller till och med omöjligt, att få fram de data som behövs för att implementera tester.

Avslutande ord

Vi har sett hur du skriver policyer för Terraform- och Kubernetes-resurser. Men Rego och OPA-baserade verktyg kan användas för många olika typer av policyvalidering. Om dina pipelines till exempel definieras i YAML, varför inte validera ändringar med en Rego-baserad policy innan du godkänner dem?

Inlärningskurvan för Rego kan vara lite brant, men eftersom Rego kan användas för så många typer av Infrastructure as Code kan det vara väl investerad tid för att undvika att använda en mängd olika policyrelaterade verktyg. Det viktiga är att din infrastruktur definieras ”som data”, inte ”som kod”. Regos metod för att göra frågor mot datastrukturer fungerar inte med ”infrastruktur som kod”.

Rego kan inkludera data från externa system som kan användas i policyer. Därför är möjligheterna för validering omfattande.

Men det finns saker som Rego-policyer inte kan göra. Rego-policyer kan ses som tester i kategorin ”komponenttester” för din infrastruktur. När det gäller dynamiska frågor som interaktioner mellan applikationskomponenter, nätverksfördröjningar och fel samt egenskapstester är Rego-policyer mindre lämpliga. Men låt inte det förringa den förbättrade hastighet och robusthet som Rego-policyer kan tillföra din infrastrukturs Continuous Delivery!

  • DevOps
  • Moln
  • CI/CD

Subscribe to our newsletter