Att utveckla och underhålla infrastruktur kan kännas skrämmande, något som Ops- och SRE-team gärna undviker – ”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ässtips: ”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, även kallat Continuous Integration.
Continuous Integration för Infrastructure as Code
Mjukvaruutvecklare har vant sig vid snabba cykler utan att kompromissa med kvalitet och stabilitet. Detta har möjliggjorts genom automatiserade tester 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. Automatiserade tester är skyddsräckena som möjliggör snabb mjukvaruutveckling.
Samma Continuous Integration-metoder 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 kommit att förvänta oss av mjukvaruutveckling.
Infrastruktur handlar inte bara om servrar
I det här sammanhanget menar vi med ”infrastruktur” allt som vi kan hantera programmatiskt med hjälp av en ”infrastructure as data”-metod. Det omfattar till exempel:
servrar
nätverk
användarautentisering och auktorisering
driftsättning av applikationer
pipeline-konfiguration
Några typiska exempel är allt som vi kan hantera med Terraform och alla Kubernetes-resurstyper.
Infrastructure as code jämfört med infrastructure as data
Termerna ”infrastructure as code” och ”infrastructure as data” används ofta synonymt, men de betyder olika saker. Dessutom används ”declarative infrastructure” ofta för att beskriva det vi här kallar ”infrastructure as data”. Men kärnan är:
We can make assertions against infrastructure defined as data. This is much more difficult with infrastructure as code.
Följande kommando skapar en VM-instans i AWS-molnet med AWS CLI. En specifik maskinavbildning (en ”AMI – Amazon Machine Image”) och maskinkonfiguration (”instance type”) anges.
Det här kommandot kan placeras i ett skript, och då har vi ”infrastructure as code”. Men tänk om vi vill att skriptet ska vara idempotent, det vill säga att skriptet bara skapar en VM även om det körs två gånger. Det kan lösas med skriptlogik, men skriptet kan till slut bli ganska komplicerat (och felbenäget).
Ett alternativ är att använda exempelvis Terraform, där vi kan definiera följande ”datastruktur” som beskriver det önskade tillståndet för vår infrastruktur. Denna ”infrastructure as 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, om än 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 måste också stödja flera språk och externa verktyg.
Föreställ dig till exempel shell-skript som använder en kombination av AWS CLI tillsammans med 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 arbetar verktyg som Open Policy Agent med data, och användningsområdena omfattar infrastructure as 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 det grundläggande verktyg som följande verktyg bygger på. OPA är en policymotor som låter oss 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 olika format, till exempel YAML och JSON. Denna 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 denna 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 integrering av OPA med Kubernetes. GateKeeper är en Kubernetes admission controller som styr Kubernetes-resursoperationer via Kubernetes API. Den kan till exempel tillåta eller neka att POD:ar skapas 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ända 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 fastställer ett visst tillstånd i den data som frågas ut.
Låt oss titta på ett enkelt exempel med följande dataset:
Vi kan fråga ut detta dataset med Rego-regler och skapa nya dataset utifrån en uppsättning villkor. I det avseendet 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:
Regelns namn är ”humans”, den tar emot indata ”name” och returnerar ett dataset baserat på tilldelningar av ”creature” i regelns brödtext.
Tilldela ”creature” valfritt värde från datasetet ”creatures” – understrecksvariabeln betyder ”loopvariabel vars värde vi inte bryr oss om”. I denna regel får ”creature” alla fyra värden från listan ”creatures”, vilket alltså påminner om en for-loop.
Fastställ att ”creature” är en människa. Om något villkor utvärderas som false innehåller det resulterande datasetet inte det specifika värdet för ”creature”.
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”. Om vi hade angett ett värde skulle vårt genererade dataset begränsas i enlighet med det:
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 något 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. Följande regeldefinition är alltså identisk med den ovan:
Vi kan sammanfoga våra två dataset med en regel som följande:
I denna regel sker sammanfogningen av datasetet favorite_food och listan över vad varje varelse gillar på sista raden. Observera återigen understrecket som används för att indexera listan ”likes” – i detta fall bryr vi oss inte om det faktiska indexet.
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å tidigare 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 – AWS-policy för taggning
Taggning är en mycket användbar metod när resurser driftsätts 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 projektexemplen i Regula) implementerar detta krav på taggning med hjälp av Conftest och Regula.
Först definierar vi en lista över AWS-resurstyper som stöder taggning (listan är förkortad för tydlighetens skull) samt en lista över taggar som vi kräver för våra resurser. aws_instance är den VM-resurstyp vi såg ovan, och följande kod, som definierar två uppsättningar, skiljer sig inte särskilt mycket från andra språk.
Därefter definierar vi en Rego-regel som sammanfogar vår lista över AWS-resurser med listan över AWS-resurstyper som kan taggas. Det här liknar mycket vårt exempel med regeln ”humans” ovan:
Därefter definierar vi en Rego-funktion som, givet en resurs, jämför taggarna för resursen med vår lista över obligatoriska taggar.
Slutligen definierar vi policyn. Utifrån resultatet av Rego-regeln/-funktionen ovan avgör vi om vi ska tillåta eller neka en viss resurs:
Exempel – policy för Kubernetes-tjänsttyp
Om ditt team driftsätter flera tjänster i Kubernetes kan ni vilja hantera lastbalanserare och TLS-certifikat globalt, till exempel med en routingarkitektur i flera nivåer. 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 kravet på Kubernetes-tjänsttyp genom att använda ”input” som källa för den Kubernetes-resurs som utvärderas.
Du kanske nu tänker: ”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 upptäcka fel före driftsättning.
Tillämpa policyer med GateKeeper
De två tidigare 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 via Kubernetes admission controller GateKeeper. GateKeeper-policyer implementeras i Rego och konfigureras i Kubernetes genom anpassade resursdefinitioner. Se till exempel GateKeeper-mallen för tjänsttypen Kubernetes för en policy som är mycket lik den ovan för Kubernetes-tjänster av typen LoadBalancer.
Vanliga saker som dina policyer kan verifiera:
Tillåt inte 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”)
Tillåt inte 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-policy-mallar kan konfigureras med parametrar, vilket gör 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 metod där den behandlas ”som data” finns det en gräns för hur mycket testning som är meningsfull. Testningen övergår snabbt till att vi testar våra egna deklarativa påståenden, som följande:
Deklarativ testning är särskilt användbar för till exempel policyer och gränssnitt mellan team. Här ansvarar olika parter för tilldelningen och testet ovan.
De ”datapåståenden” som visas i det här blogginlägget kan också bara göra påståenden baserat på tillgängliga data. Komplexa system kan ha data på många olika platser och i många olika format, ofta med dataåtkomst uppdelad 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 verktyg baserade på OPA 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 olika 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 fråga datastrukturer fungerar inte med ”infrastructure as code”.
Rego kan inkludera data från externa system som kan användas i policyer. Valideringarnas omfattning är därför bred.
Men det finns saker som Rego-policyer inte kan göra. Rego-policyer kan ses som ett slags komponenttest för din infrastruktur. När det gäller dynamiska frågor som interaktioner mellan applikationskomponenter, nätverkslatens och fel samt egenskapstester är Rego-policyer mindre lämpade. Men låt inte det förringa den förbättrade hastighet och robusthet som Rego-policyer kan tillföra din Continuous Delivery för infrastrukturen!
- DevOps
- Cloud
- CI/CD
Subscribe to our newsletter
Related blogs