sobota 27. dubna 2013

Evoluce Continuos Integration infrastruktury v GoodData

Několik posledních měsíců jsem spolupracoval na rozvojí naší Continuos Integration (zkráceně CI) infrastruktury v GoodData. V tomhle článku bych se chtěl podělit o koncept, který jsme vymysleli a nakonec uvedli v život.

Motivace

K změnám v CI infrastruktuře nás přivedly dvě zásadní události. K tomu, aby je bylo možné pochopit, je potřeba trochu kontextu. GoodData byly historicky aplikace s monolitickou architekturou. Díky přepisu na Platform As A Service došlo k částečným změnám architektury. Někdy kolem roku 2009 byl hlavním jazykem Perl pro backend a JavaScript pro frontend. Tomu odpovídal i verzovací systém a CI infrastruktura okolo. Zároveň na celém produktu pracovalo kolem 15 vývojářů. Postupem času došlo k růstu celých GoodData a to na všech frontách - objemem zdrojových kódů počínaje a počtu vývojářů konče.

Tento růst celkem přirozeně odhalil úzká hrdla v produktivitě týmů, které jsme se rozhodli řešit na úrovni architektury a změnami v organizační struktuře. V dobré víře jsme provedli změny v architektuře, která byly mnohem více orientovaná na služby. Předpokládali jsme, že týmy pracující na službách s velmi malým technologickým dluhem, budou schopny produkovat a hlavně doručovat nové vlastnosti mnohem rychleji než ve 14 denních intervalech a hlavně nezávisle na službách a týmech, které měly vetší technologický dluh a nebo jejich týmy nebylo z různých důvodů patřičně personálně obsadit.

Cesta do pekla je dlážděna dobrými úmysly

Výsledek na produktivitu týmu byl ovšem přesně opačný než jsme očekávali. Jednou z hlavních příčin byla CI infrastruktura, která stále odpovídala z větší části monolitické aplikaci. Jenže zatímco v roce 2009 jsme měli jeden Git repozitář, dva programovací jazyky a 15 vývojářů, na začátku roku 2013 jsme měli na dvě desítky Git repozitářů, čtyřnásobný počet vývojářů a pět programovacích jazyků.




Teď uděláme malý úkrok stranou k drobné paralele, která nám pomůže lépe pochopit situaci. Představte si, že jste šéf konstrukční kanceláře, který navrhuje a vytváří prototypy aut. Vytvoříte nějaký plán, podle kterého sestavíte první verzi prototypu a tu otestujete. Ejhle je potřeba tu vyměnit podvozek, tady je potřeba jiný typ karoserie. Uděláte další verzi prototypu a ten zase otestujete. Samozřejmě potřebujete schéma auta, které zachycuje, z jakých konkrétních dílu je auto sestavené. Jenže protože vývoj prototypu je iterativní proces, nepotřebujete zaznamenat, že váš prototyp má čtyři kola, výfuk, karoserii, volant, motor a tak dále, ale i verze jednotlivých součástek.


Bez těchto verzí by bylo složité poslat auto do produkční výroby, protože by si nikdo nebyl jistý tím, co se opravdu testovalo a co bylo schválené, případně co se použilo pro jakou verzi prototypu. Každou složitější součástku navíc designuje jiné oddělení vaší kanceláře. Je řádově složitější navrhnout a vyladit spalovací motor oproti blinkru, případně je potřeba provést pouze malé změny dané součástky, proto mají vaše oddělení jinou rychlost v dodávání nových verzí. Vývoj prototypu je nikdy nekončící proces, odladěná verze jde do produkční linky a vy pracujete na další verzi. Protože o práci není nouze, máte různé verze prototypů v různém stavu. V produkční lince je verze A, vy finišujete verzi B a zároveň máte solidně rozdělanou verzi C a oddělení spalovacích motorů možná už pracuje na verzi D.


Do předchozí paralely si dosaďte místo součástky službu (komponentu), místo prototypu GoodData, místo verze A,B,C,D konkrétní release, místo stroje na výrobu součástek build, místo pilota na testovacím okruhu automatické testy, místo schváleni QA a jsme zpátky v uvozovkách doma u softwarového vývoje.



Původní infrastruktura

Původní infrastruktura byly v Jenkins a měla tři hlavní části takzvanou build pipeline, integration pipeline a deploy pipeline.


Build pipeline

Build pipeline se stará o build a základní zajištění kvality. Výstupem je vždy jeden nebo více RPM balíčků. Zajištění kvality se liší v závislosti na tom, co Jenkins pro danou technologii podporuje a na tom, jaké typy testů jsou k dispozici. Absolutní minimum pro zajištění kvality představují unit testy. Platí pravidlo, že pokud testy neprojdou, není výstup pipeline poslán dále do integration pipeline. Pouštět testy v této early fází je velmi levné z pohledu časové náročnosti.


Integration pipeline

Integration pipeline se stará o spuštění funkčních testů proti nainstalované verzi GoodData. Zde již není místo pro žádné mocky a dochází k reálné integraci služeb. Funkční testy jsou napsané proti REST rozhraní a provádějí v podstatě totožné operace jako jakýkoliv klient GoodData včetně vlastní GoodData aplikace v prohlížeči.


Deploy pipeline

Deploy pipeline slouží k nainstalování GoodData na prostředí reprezentované jedním počítačem(personal instance, QA instance, Jenkins slave) nebo do clusteru topologicky odpovídajícímu produkčnímu nasazení.




Poznámka: ozubená kolečka jsou jednotlivé joby v Jenkinsu


Hlavní nedostatky

S obrovským počtem změn docházelo často k tomu, že celé řešení v hlavní vývojové větvi bylo rozbité. Pokud chtěli vývojáři vytvořit vývojové prostředí z aktuálního stavu hlavní vývojové větve, byla to jako hrát ruskou ruletu. Vzhledem k tomu, že vytvoření vývojového prostředí za použití Deploy pipeline trvalo přibližně hodinu, bylo to velmi frustrující.

Automatické testy v rámci v Integration pipeline neběhaly na stejném prostředí, které používali vývojáři. To mělo za následek, že fungující automatický test na vývojářském prostředí neměl patřičnou vypovídající hodnotu. Navíc CI infrastruktura pro tyto automatické integrační testy byla velmi nespolehlivá. Docházelo k mnoha false-positive případům a situacím, kdy vývojář tvrdil "že mu to prostě na vývojovém prostředí funguje". To vedlo k nedůvěře a frustraci vývojářů.

V CI serveru (Jenkins) existovalo velké množství jobů, které svým způsobem ukazovali stav (kondici) hlavní či release větvě. Chybělo ovšem místo ukazující přehledně stav, tedy především výsledky testů, ze kterého by bylo patrné v jakém stavu se nacházíme.


Z těchto nedostatků nebylo zas tolik složité určit, co je potřeba změnit, abychom je překonali.


  • Vývojové prostředí musí odpovídat prostředí, které používá CI infrastruktura pro spuštění automatických testů. Ideálně to musí být jedno a to samé prostředí.
  • Musí existovat způsob, jak označit konfiguraci (Puppet) a kombinaci RPM balíčků, které projdou Integration pipeline, aby bylo možné tenhle otisk vzít a kdykoliv později z něj vytvořit vývojářské nebo QA prostředí, případně jej rovnou nasadit na produkci.
  • Musí existovat způsob, pomocí kterého půjde integračně testovat služby s různou rychlostí vývoje, bez toho aniž bychom je vytvářet větve.

Koncept Release train

Díky tomu, že jsme používali různé Git repozitáře, bylo zřejmé, že integrace musí probíhat na úrovní binárních balíčků (RPM), které vypadávají z Build pipeline jednotlivých služeb. Tak vznikl Release train, kde se měli jednotlivé balíčky shromažďovat. Release train si představte jako adresář, do které se po doběhnutí Build pipeline zkopírují binární balíčky, které byly vyprodukované. Do Release train padají nezávisle na sobě binární balíčky všech služeb. V jednu chvíli existují vždy tři aktivní Release trainy.

  • produkční release train - představuje verzi GoodData nasazenou na produkci. Padají tam především bugfixy, které je potřeba při nejbližší příležitosti nasadit.
  • stable release train - představuje další verzi GoodData, která bude nasazena během následujících 14 dnů.
  • master release train - vývojová verze GoodData, která bude nasazena v horizontu 30 dnů.

Assembly descriptor

Release train představuje pouze kontejner reprezentující daný release. K tomu abychom viděli, v jaké kondici se právě nachází, a dokázali vytvořit snapshot reprezentující tento stav, je potřeba popis, který jednoznačně určí verze binárních balíčků. Tomuto popisu říkáme Assembly descriptor a ten kromě verzí balíčků obsahuje i výsledky testů a další metadata. Assembly descriptor je v podstatě curriculum vitae pro daný snapshot. Vzniká ve ve chvíli kdy se balíčky kopírují do Release trainu a je jím řízena Integration pipeline i Deploy pipeline.

Assembly descriptor


{
  "artifactGroups": [
    {
      "name": "client",
      "artifacts": ["s3://gdc-dev/release-train-repo/r91/gdc-client-1.master-0.14468.3a72e54.noarch.rpm"]
    },
    
    {
      "name": "webapp",
      "artifacts": ["s3://gdc-dev/release-train-repo/r91/gdc-webapp-1.master-23.39028.7676733.noarch.rpm"]
    },
    ....
  ],
  "creationDate": 1366975570871,
  "id": "s3://gdc-dev/release-train-repo/r91/assembly-descriptor-440abc69-27b3-4e58-b4d0-1b8b5a1ed78b.json",
  "labels": [],
  "lastModifiedBy": null,
  "lastModifiedDate": 1366985563243,
  "lifecycleState": "FINISHED",
  "memo": null,
  "puppetVersion": "git@github.com:gooddata/puppet.git#8e9b0b6d2946eee41f91aa767c421da241c2a7d6",
  "tests": [
    {
      "skipCount": 0,"failCount": 0,"totalCount": 0,
      "type": "DEPLOY",
      "result": "PASSED",
      "build": "https://ci.intgdc.com/job/SC%20(M3)%20-%20Deploy%20Instance%20for%20Tests%20-%20pipe/939/"
    },
    {
      "skipCount": 0,"failCount": 0,"totalCount": 64,
      "type": "SMOKE",
      "result": "PASSED",
      "build": "https://ci.intgdc.com/job/SC%20(M3)%20-%20Run%20Smoke%20tests%20-%20pipe/669/"
    },
    {
      "skipCount": 0,"failCount": 0,"totalCount": 3645,
      "type": "FUNCTIONAL",
      "result": "PASSED",
      "build": "https://ci.intgdc.com/job/SC%20(M3)%20-%20Integration%20tests%20-%20pipe/589/"
    }
  ],
  "version": 1
}
 

V Release trainu vznikne denně přibližně 20 Assembly descriptoru někdy i více v závislosti na aktivitě vývojářů. Ne všechny se ovšem testují, kompletní průchod Integration pipeline trvá dvě a půl hodiny. Z tohoto důvodu se automaticky naplánuje k otestování vždy nejnovější Assembly descriptoru, který vzniknul za posledních 60 minut. Integration pipeline má na vstupu Assembly descriptor, podle kterého se vytvoří testovací prostředí. Následně se pustí baterie smoke testů a funkčních testů. Jejich výsledek se zapíše zpátky do Assembly descriptoru. Celé je trochu složitější, ale následující obrázek vám pomůže získat představu.




Viditelnost a transparentnost

Release train a Assembly descriptor jsou jenom technické prostředky, pomocí kterých jsme se snažili odstranit hlavní nedostatky předchozí verze. K nim bylo potřeba vytvořit grafické rozhraní. Toto rozhraní mělo umožnit následující:

  • Viditelnost stavu vývoje GoodData na jednom místě - jak a na kolik nám procházejí testy
  • Vytvoření vývojářského prostředí z otestovaného Assembly descriptoru

Uživatelské rozhraní vzniklo jako view do Jenkins a postupně jsme jej rozšířili o další vychytávky jako je podpora labels, porovnání dvou assembly descriptorů atd.




Vývojář si může při nahlédnutí do Release trainu zvolit nejlepší dostupný assembly descriptor a z něj vytvořit vývojové prostředí. Protože jsou zapečené verze balíků a konfigurace, má skoro jistotu, že se vše povede. Zároveň si může odladit vlastní kód a spuštěním testů odhalí případné regrese v chování, protože to samé prostředí používá i Integration pipeline. Pokud je potřeba získat další informace o assembly descriptoru, například výsledky testů, je možné se podívat na detailní popis, ze kterého již vedou odkazy na konkrétní výsledky.




Architektura

Celé řešení má dvě hlavní části - backendové joby a uživatelské rozhraní. Obě dvě části běhají v Jenkins. Uživatelské rozhraní je klasický Jenkins plugin. Backend joby, které se používají v jednotlivých pipelines, jsou klasické skripty. Skoro všechny joby spolupracující s release trainy a operující s assembly descriptory jsou napsané v Jave. V Jave existuje doménový model reprezentující hlavní entity a další abstrakce. Dále je k dispozici API pro operace nad vlastním úložištěm dat a API pro operace na vyšší úrovni abstrakce např. listování všech assembly descriptorů. Toto API se využívá i v pluginu s uživatelským rozhraním, právě proto padla volba na Javu. Kromě jobů, které přímo participují v celém flow assembly descriptoru, existují servisní joby, které pomáhají držet celé řešení pohromadě např. garbage collector.


Technologie

Všechny data, RPM balíčky, assembly descriptory a další metadata jsou uložena na Amazon S3. Vlastní release train je jenom klíč v bucketu. Pokud jste nikdy nepracovali s S3, pak si klíč představte jako adresář souborového systému. Assembly descriptor je obyčejný JSON dokument. Velká část ovládání uživatelského rozhraní je napsaná v JavaScriptu pomocí frameworku YUI. Integration pipeline a Deploy pipeline jsou napsané v bashi. Protože S3 není žádná databáze a například zobrazení assembly descriptoru znamená jejich načtení a deserializaci, dost agresivně se cacheuje pomocí EHcache.


Závěr

Na celém řešení jsme prakticky pracovali ve dvou lidech, společně se mnou ještě Radek Chromý, kterého musím touhle cestou zmínit. Celý listopad a prosinec nám trval jenom vývoj první verze, na které jsme validovali životaschopnost celého konceptu. Další dva až tři měsíce jsme celé řešení dolaďovali a uvolňovali k širšímu použití. Problémy, které jsme měli s původní CI infrastrukturou se opravdu podařilo odstranit. Došlo k výraznému zlepšení stability testů, protože vývojáři se už nemohli v případě padajících testů vymlouvat na rozdílnou infrastrukturu. Zlepšila se i spolehlivost vytváření vývojářských prostředí, jak ukazuje následující graf.




Podle mého názoru kvalitu návrhu každého systému prověří až požadavky na jeho další rozšíření. Zatím se zdá, že všechny změny, které se chystáme dělat, umožňují začlenění do konceptu Release trainu. Mimochodem stále nabíráme kdyby vás lákalo na tomhle systému pracovat a dále jej rozvíjet.