Blog

Miten DevOps-käytäntöjä sovelletaan Infrastructure as Codeen

DEC 17, 2021

Infrastruktuurin kehittäminen ja ylläpito voi tuntua pelottavalta, jolloin Ops- ja SRE-tiimit saattavat mieluummin välttää sitä – ”Älä koske toimivaan järjestelmään.” Näin ei kuitenkaan tarvitse olla!

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.

Kolme keskeistä DevOps-käytäntöä vaikuttavat ohjelmistotoimitukseemme ja operatiiviseen suorituskykyymme (lukuvinkki: Kief Morrisin ”Infrastructure as Code”):

  • Määrittele kaikki koodina

  • Testaa ja toimita kaikki keskeneräinen työ jatkuvasti

  • Rakenna pieniä, yksinkertaisia osia, joita voit muuttaa itsenäisesti

Jos rakennat modernia ohjelmistoa, nämä eivät varmasti tule yllätyksenä. Mutta tiesitkö, että ne pätevät myös modernin infrastruktuurin kehittämiseen?

Tässä blogikirjoituksessa opit testaamaan infrastruktuuria jatkuvasti eli toteuttamaan Continuous Integrationia.

Infrastructure as Coden Continuous Integration

Ohjelmistokehittäjät ovat tottuneet nopeisiin sykleihin laadusta ja vakaudesta tinkimättä. Tämä on saavutettu testaamalla kaikki koodimuutokset automaattisesti, kun ne commitoidaan Gitiin. Muutoksia, jotka eivät läpäise automaattisia testejä, ei saa yhdistää koodikantaamme. Automaattinen testaus on suojakaide, joka mahdollistaa nopean ohjelmistokehityksen.

Samoja Continuous Integrationin käytäntöjä voidaan soveltaa infrastruktuuriin. Voimme määritellä infrastruktuurimme ominaisuuksia koskevia käytäntöjä. Käytännöt voidaan validoida testeillä, jolloin saavutamme ohjelmistokehitykseltä odottamamme nopeuden, vakauden ja laadun yhdistelmän.

Infrastruktuuri ei tarkoita vain palvelimia

Tässä yhteydessä infrastruktuurilla tarkoitamme kaikkea, mitä voimme hallita ohjelmallisesti ”infrastruktuuri datana” -lähestymistavalla. Se sisältää esimerkiksi:

  • palvelimet

  • verkkoyhteydet

  • käyttäjien todentamisen ja valtuutuksen

  • sovellusten käyttöönotot

  • putkien konfiguroinnin

Tyypillisiä esimerkkejä ovat kaikki Terraformilla hallittavat kohteet sekä kaikki Kubernetesin resurssityypit.

Infrastruktuuri koodina vs. infrastruktuuri datana

Termejä ”infrastruktuuri koodina” ja ”infrastruktuuri datana” käytetään usein synonyymeinä, vaikka ne tarkoittavat eri asioita. Myös termiä ”deklaratiivinen infrastruktuuri” käytetään usein kuvaamaan sitä, mitä tässä tarkoitamme ”infrastruktuurilla datana”. Keskeinen asia on kuitenkin tämä:

Voimme tehdä väittämiä datana määritellystä infrastruktuurista. Infrastruktuurin ollessa koodia tämä on huomattavasti vaikeampaa.

Seuraava komento luo VM-instanssin AWS-pilveen AWS CLI:n avulla. Komennossa määritetään tietty konekuva (”AMI - Amazon machine image”) ja koneen konfiguraatio (”instance type”).

Komento voidaan sijoittaa skriptiin, jolloin meillä on ”infrastruktuuri koodina”. Kuvitellaan kuitenkin, että haluamme skriptin olevan idempotentti eli että skriptin ajaminen kahdesti luo vain yhden VM:n. Tämä olisi mahdollista toteuttaa skriptilogiikalla, mutta lopulta skriptistä saattaisi tulla varsin monimutkainen ja virhealtis.

Vaihtoehtoinen tapa on käyttää esimerkiksi Terraformia, jolla voimme määritellä seuraavan ”datarakenteen”. Se määrittää infrastruktuurimme tavoitetilan. Tämän ”infrastruktuurin datana” etuna on, että Terraform hallitsee kaiken tarvittavan logiikan tavoitetilan ja infrastruktuurin todellisen tilan yhteensovittamiseksi. Kubernetes toimii vastaavalla tavalla, joskin erilaisilla ”datarakenteilla” (eli ”Kubernetes resource YAML” -tiedostoilla).

Sen lisäksi, että esimerkiksi Terraform ja Kubernetes hoitavat osan infrastruktuurin hallinnan monimutkaisuudesta, hyötyä tuo myös tapa, jolla infrastruktuuri määritellään. Kun infrastruktuuri on koodia, koodin vaikutuksia ja oikeellisuutta on yleensä hyvin vaikea arvioida. Tällaisen tarkistuksen tekevän työkalun pitäisi myös tukea useita ohjelmointikieliä ja ulkoisia työkaluja.

Kuvitellaan esimerkiksi shell-skriptaus, jossa AWS CLI:tä käytetään yhdessä jq:n, sedin, awkin, grepin ja muiden työkalujen kanssa. Datarakennelähestymistavassa tämä on huomattavasti yksinkertaisempaa, sillä datarakenne on yleensä melko yksinkertaisessa muodossa, ja vain datarakenteen yksittäisten objektien skeemat ovat toimialakohtaisia (esimerkiksi AWS:n Terraform-resurssit tai Kubernetes-resurssit).

Tämän vuoksi Open Policy Agentin kaltaiset työkalut toimivat datan kanssa, ja käyttötapauksiin kuuluu infrastruktuuri datana.

Open Policy Agent ja siihen liittyvät työkalut

Katsotaan nyt, miten näillä työkaluilla voidaan toteuttaa käytäntöjä infrastruktuurillemme datana:

Open Policy Agent (OPA) on perustyökalu, jonka päälle seuraavat työkalut on rakennettu. OPA on käytäntömoottori, jonka avulla voimme määritellä käytännöt koodina ja testata infrastruktuuriamme niitä vasten. Käytännöt määritellään Rego-nimisellä kielellä.

Conftest on OPA:n integroiva työkalu, joka vastaanottaa jäsenneltyä dataa useissa muodoissa, kuten YAML- ja JSON-muodossa. Data voi olla esimerkiksi Kubernetes-resursseja YAML-muodossa tai Terraform-suunnitelma JSON-muodossa, ja Conftestin avulla voimme arvioida Rego-käytäntöjä tätä dataa vasten. Conftest sopii erinomaisesti infrastruktuurin validointiin osana CI-putkea.

Regula on Rego-kirjasto, jota voidaan käyttää yhdessä Conftestin kanssa ja joka on suunniteltu erityisesti Terraformin infrastruktuuridatalle.

GateKeeper on OPAn ja Kubernetesin integraatio. GateKeeper on Kubernetes admission controller, joka hallitsee Kubernetes-resurssien toimintoja Kubernetes API:n kautta. Se voi esimerkiksi sallia tai estää PODin luomisen sen perusteella, miten OPA arvioi Rego-käytännöt PODin määrittelyä vasten. GateKeeperiä ei siis käytetä CI-prosessin aikana (siellä käytämme Conftestiä), vaan sitä tulisi käyttää samoilla käytännöillä kuin Conftestiä, jotta voidaan hallita sitä, mitä Kubernetesissa todella otetaan käyttöön.

Rego-kieli

Rego on datakyselykieli, jonka ytimessä ovat säännöt. Säännöt koostuvat kyselyistä, joilla vahvistetaan tietty tila kyseltävässä datassa.

Tarkastellaan yksinkertaista esimerkkiä seuraavan tietojoukon avulla:

Voimme kysellä tätä tietojoukkoa Rego-säännöillä ja luoda ehtojoukon perusteella uusia tietojoukkoja. Tässä mielessä Rego-säännöt muistuttavat tietokantakyselyjä. Seuraava sääntö luo tietojoukosta tietojoukon, joka sisältää kaikki ihmislajiset olennot:

Tämän säännön koodirivit tulee lukea seuraavasti:

  1. Säännön nimi on ”humans”. Se ottaa syötteenä ”name”-arvon ja palauttaa tietojoukon, joka perustuu säännön rungossa tehtyihin ”creature”-sijoituksiin.

  2. Anna muuttujalle ”creature” mikä tahansa arvo ”creatures”-tietojoukosta – alaviivamuuttuja tarkoittaa silmukkamuuttujaa, jonka arvosta emme välitä. Tämän säännön sisällä ”creature” saa kaikki neljä arvoa ”creatures”-listasta. Tämä muistuttaa siis jossain määrin for-silmukkaa.

  3. Vahvista, että ”creature” on ihminen. Jos jokin ehto arvioituu epätodeksi, tuloksena syntyvä tietojoukko ei sisällä kyseistä ”creature”-arvoa.

  4. Aseta nimi kyseisestä ”creature”-arvosta.

Huomaa, ettemme määrittäneet syötteelle ”name” arvoa. Tämä tarkoittaa, ettei muuttuja ole sidottu tiettyyn arvoon, joten säännöt ratkaistaan ilman ”name”-arvoon kohdistuvaa rajoitetta. Jos olisimme määrittäneet arvon, luomamme tietojoukko olisi vastaavasti rajattu:

Regossa yhtäsuuruusmerkki ”=” tarkoittaa sekä vertailua että sijoitusta, ja sijoitus voi tapahtua sekä vasemmalta oikealle että oikealta vasemmalle. Sitä tulisi siis pitää matemaattisena yhtälönä. Edellä olevan säännön seuraava rivi oli sijoitus ensimmäisessä sääntöjen suorituksessa, koska emme määrittäneet ”name”-arvoa, ja vertailu jälkimmäisessä sääntöjen suorituksessa.

Lauseiden järjestyksellä tai edes yhtäsuuruusmerkin ”=” vasemmalla ja oikealla puolella olevien elementtien järjestyksellä ei ole merkitystä. Seuraava säännön määrittely on siis sama kuin yllä:

Voimme yhdistää kaksi tietojoukkoamme seuraavanlaisella säännöllä:

Tässä säännössä favorite_food-tietojoukon ja kunkin olennon pitämien asioiden listan yhdistäminen tapahtuu viimeisellä rivillä. Huomaa jälleen ”likes”-listan indeksointiin käytetty alaviiva – tässä tapauksessa emme välitä varsinaisesta indeksistä.

Tyypillinen tapa työskennellä Regolla on rakentaa perussääntöjä, jotka yhdistetään myöhemmin edistyneemmiksi säännöiksi. Seuraava sääntö yhdistää kaksi edellistä sääntöä ja palauttaa luettelon ihmisistä, jotka pitävät suosikkilistalla olevasta ruoasta:

Kun suoritamme tämän säännön, saamme:

Esimerkki – AWS:n taggauskäytäntö

Taggaus on erittäin hyödyllinen tekniikka resurssien käyttöönotossa AWS-pilvialustalla. Kuvitellaan, että yrityksellämme on käytäntö, jonka mukaan kaikki resurssit on tagattava ”Owner”-tagilla, joka määrittää tietystä resurssista vastuussa olevan henkilön. Miten tämä onnistuu Regolla?

Seuraava käytäntö (mukailtu Regula-projektin esimerkeistä) toteuttaa tämän taggausvaatimuksen Conftestin ja Regulan avulla.

Määrittelemme ensin luettelon taggausta tukevista AWS-resurssityypeistä (luetteloa on lyhennetty selkeyden vuoksi) sekä luettelon tageista, jotka resursseillamme on oltava. aws_instance on edellä näkemämme VM-resurssityyppi, eikä seuraava kahden joukon määritelmä näytä kovin erilaiselta kuin muissa kielissä.

Seuraavaksi määrittelemme Rego-säännön, joka yhdistää AWS-resurssiluettelomme tagattavissa olevien AWS-resurssityyppien luetteloon. Tämä muistuttaa paljon yllä olevaa ihmiset-sääntöesimerkkiämme:

Seuraavaksi määrittelemme Rego-funktion, joka vertailee annetun resurssin tageja vaadittujen tagien luetteloomme.

Lopuksi määrittelemme käytännön. Päätämme yllä olevan Rego-säännön/-funktion tuloksen perusteella, sallimmeko vai estämmekö tietyn resurssin:

Esimerkki – Kubernetes-palvelutyypin käytäntö

Jos tiimisi ottaa käyttöön useita palveluja Kubernetesissa, saatat haluta hallita kuormantasaajia ja TLS-varmenteita keskitetysti (esimerkiksi monitasoisen reititysarkkitehtuurin avulla). Tällöin haluamme varmistaa, etteivät tiimit ota käyttöön LoadBalancer-tyyppisiä Kubernetes-palveluja. Miltä tätä varten laadittu Rego-käytäntö näyttäisi?

Seuraava käytäntö toteuttaa tämän Kubernetes-palvelutyyppiä koskevan vaatimuksen käyttämällä inputia arvioitavan Kubernetes-resurssin lähteenä.

Saatat nyt pohtia: ”Miksi emme vain käyttäisi Kubernetes RBACia rajoittamaan käyttöönotettavia kohteita?”

Erona on se, että Rego-käytännöillä validoimme infrastruktuurimäärityksemme ennen käyttöönottoa. Tämä poikkeaa käytännön toimeenpanosta vasta infrastruktuurin käyttöönoton jälkeen. Toimintatapa muistuttaa sitä, miten ajamme testejä osana CI-putkia, jotta havaitsemme virheet ennen käyttöönottoa.

Käytäntöjen toimeenpano GateKeeperillä

Kaksi edellistä käytäntöesimerkkiä osoittivat, miten infrastruktuuriresursseja voidaan validoida esimerkiksi osana CI-putkea ennen kuin muutokset hyväksytään käyttöönotettaviksi. Rego-käytännöt voidaan toimeenpanna Kubernetesissa vastaavasti GateKeeper-Kubernetes-admission controllerin avulla. GateKeeper-käytännöt toteutetaan Regolla ja määritetään Kubernetesissa mukautettujen resurssimääritysten avulla. Katso esimerkiksi Kubernetes Gatekeeper Service type template, jonka käytäntö muistuttaa läheisesti yllä olevaa Kubernetes LoadBalancer -palvelutyyppejä koskevaa käytäntöä.

Tyypillisiä asioita, joita käytäntösi voivat tarkistaa:

  • Estä hostpath-taltiot

  • Konttien resurssirajat ja -pyynnöt on asetettu, ja niiden arvot ovat järkeviä

  • Konttikuvat haetaan tietyistä rekistereistä, ja konttitagit ovat mahdollisesti myös tiivisteitä eivätkä muuttuvia tageja (kuten ”1.0”)

  • Estä tietyt node port -alueet

  • Vaaditut ja/tai yksilölliset tagit

  • Ingress-polut ovat kelvollisia ja/tai yksilöllisiä

  • PODeilla on readiness- ja liveness-probet

  • Kontteja ei ajeta root-käyttäjänä

GateKeeper-käytäntömallit voidaan määrittää parametrien avulla, joten eri namespaceille voidaan määrittää erilaisia käytäntöjä. Katso esimerkiksi GateKeeperin ContainerLimits-käytäntö.

Deklaratiivisten testien rajoitukset

Kun infrastruktuuri määritellään deklaratiivisella ”datana”-lähestymistavalla, järkevän testauksen määrällä on rajansa. Testauksessa päädytään nopeasti testaamaan omia deklaratiivisia määrityksiämme, kuten seuraavassa:

Deklaratiivinen testaus on hyödyllisintä esimerkiksi käytäntöjen ja tiimien välisten rajapintojen yhteydessä. Tällöin yllä olevasta määrityksestä ja testistä vastaavat eri osapuolet.

Tässä blogikirjoituksessa esitetyt data assertions -tarkistukset voivat tehdä tarkistuksia vain käytettävissä olevasta datasta. Monimutkaisissa järjestelmissä dataa voi olla monissa eri paikoissa ja muodoissa, ja pääsy dataan on usein rajattu tietoturvasyistä. Tämä vaikeuttaa tai jopa estää testien toteuttamiseen tarvittavan datan saamisen.

Lopuksi

Olemme nähneet, miten Terraform- ja Kubernetes-resursseille kirjoitetaan käytäntöjä. Regoa ja OPA-pohjaisia työkaluja voidaan kuitenkin käyttää monenlaiseen käytäntöjen validointiin. Jos esimerkiksi pipelinet on määritelty YAMLissa, miksi et validoisi muutoksia Rego-pohjaisella käytännöllä ennen niiden hyväksymistä?

Regon oppimiskynnys voi olla hieman jyrkkä, mutta koska sitä voidaan käyttää niin monenlaisessa Infrastructure as Codessa, siihen käytetty aika voi olla sen arvoista, jos näin vältyt monien erilaisten käytäntöihin liittyvien työkalujen käytöltä. Olennaista on, että infrastruktuurisi on määritelty datana eikä koodina. Regon data structure query -lähestymistapa ei toimi infrastructure as code -mallissa.

Rego voi sisältää ulkoisista järjestelmistä haettua dataa, jota voidaan hyödyntää käytännöissä. Validointien kattavuus on siis laaja.

Rego-käytännöillä ei kuitenkaan voi tehdä kaikkea. Niitä voidaan pitää infrastruktuurisi komponenttitesteinä. Rego-käytännöt soveltuvat huonommin dynaamisten asioiden, kuten sovelluskomponenttien välisten vuorovaikutusten, verkkoviiveiden ja -vikojen sekä ominaisuustestien, käsittelyyn. Älä kuitenkaan anna sen vähentää Regon käytäntöjen infrastruktuurisi Continuous Deliveryyn tuomaa nopeutta ja luotettavuutta!

  • DevOps
  • Cloud
  • CI/CD

Subscribe to our newsletter