Dockerizace Maven testů v CI Jenkins

Už to bude pár let, co se mezi námi objevil pojem Docker a následná dockerizace všeho živého a neživého v IT. Technologie Dockeru se už pomalu začíná usazovat a koketuje s ní stále více firem. Nedávno jsme se rozhodli i my ve firmě, že tuto technologii vyzkoušíme a zařadíme do běžného workflow našich aplikací.

Předpokládám, že všichni víte, co je to Docker. Pokud ne, doporučuji si poslechnout přednášku Martina Damovského a Radima Dana Pánka.

Co od toho očekáváme?

To je jednoduchá otázka s jednoduchou odpovědí – zjednodušení práce.

Co nám může Docker přinést?

  • Pomoc s integračním testování
  • Sjednocení verzí a konfigurace služeb na dev/test/prod
  • Síťovou “virtualizaci” mezi kontejnery
  • Usnadnění deploymentu
  • Rychlejší spuštění archaických aplikací
  • Sdílení stavů aplikací a databází mezi vývojem a testem

Sjednocení verzí

Vzhledem k tomu, že je naše firma už nějakou chvilku na trhu, disponuje velkou škálou klientských aplikací. Některé aplikace běží například na Java 5 nebo starších verzí MySQL 5. Tyto nástroje jsou v dnešní době už těžko sehnatelné a velmi komplikovaně spustitelné na platformách, jako je Mac OSX a jiné.

Pokud na takových aplikacích probíhal i nyní nový vývoj, musel vývojář používat verzi nástroje jinou, než na které jede aplikace na produkci. Tento stav je nežádoucí, jelikož nástroje se mohou verze od verze lišit. Například tomu tak je u MySQL 5. Zde Oracle pod odkoupení Sun Microsystems upravil SQL definice.

Další kapitolou jsou testy. Máme CI Jenkins, kde probíhá testování opět velké škály aplikací. Bohužel se ale pro spouštění Unit testů používá jediná, pro testy vyhrazená, MySQL databáze. Pro řadu aplikací je tato MySQL databáze starší než ta v produkci.

Vše, co jsem popsal, mohlo a také nejednou zapříčinilo problém. Proto dockerizace nástrojů jako MySQL, Elasticsearch a následné používání konkrétní verze jejich image, zajistí určitou chtěnou uniformitu prostředí. V tomto případě jsme začali sestavovat vlastní image nástrojů. Můžeme tak používat stejnou verzi nástroje na produkci i pro testy a vývoj.

Docker pro usnadnění deploymentu

To je jedna z hlavních výhod, kterou se Docker prezentuje. Usnadní deploy aplikace na server. Má to ale jeden podstatný háček. A to, že nativně instalovaná aplikace bude mít vždy vyšší výkon. Možná nepatrně, ale bude. Proto se nedoporučuje dockerizovat na produkci například databázi.

Naše aplikace, ale používají kromě databází další podpůrné nástroje. Používáme vlastní mikroslužby pro generování PDF nebo streamování videa. A přesně deployment těchto aplikací je učebnicový příklad využití Dockeru. Pomocí CI si sestavíme image s tímto nástrojem a pro vývoj, testy, produkci můžeme distribuovat už pouze jen home-build image. Rozjetí takové aplikace je pak otázkou chvilky i pro neznalého.

Starodávné aplikace a jejich spuštění

Jako firma s dlouhou historií máme mnoho postarších projektů, kterým stále poskytujeme podporu. Postupem let se informace o projektech vytratily. Jejich současné rozjetí na vývoji, testech a produkci je poměrně komplikovanou záležitostí. Pomocí Dockeru si můžeme vystavět runtime prostředí pro aplikaci, včetně starších knihoven a nástrojů jako je Java 5, MySQL atd. a snadno jej spustit.

Síťová virtualizace

Kontejnery si mezi sebou mohou povídat. A mohou si povídat tak, že nemusí blokovat TCP/UDP porty hostitelského systému. Proto si můžeme sestavit “farmičky” dockerových kontejnerů, které si mezi sebou rozumí, a nás zajímá pouze jen finální výstup. A to buď CLI, nebo např. HTTP TCP port, socket.

Takových “farmiček” si můžeme spustit na hostitelském systému hned několik. A to právě proto, že neblokují hostitelské systémové prostředky. Síťová virtualizace uvnitř Dockeru je podobná síti postavené mezi plně virtualizovanými systémy. V každé “farmičce” tak můžeme mít MySQL, Elasticsearch, RabbitMQ, Tomcat a další dopňkové mikroslužby. Ale navenek se “farmička” hlásí pouze tak, jak potřebujeme.

Sdílení stavu databází mezi vývojem a testem

Taky jste určitě zažili situaci, kdy vás kolega požádal o data. Museli jste provést SQL dump, přenést soubor, kolega si ho musel naimportovat a doufat,že nástroj, kterým provádíte export/import, nepoužívá žádné zběsilosti. Data musela být integritní a DDL spolu s DML částmi musela být ve správném pořadí.

Pokud něco z toho neklaplo, hodiny jste hledali problém.

S tímto může opět pomoci Docker. Na vývoji je možné provozovat databázi dockerizovanou. Následný commit a push do firemní registry je pak otázka chvilky. Kdokoli pak bude vaše data potřebovat, jednoduše si je s příslušným databázovým nástrojem stáhne z registry a spustí. Tento postup je možné následně aplikovat minimálně mezi vývojáři, testovacím strojem, testery atd…

Integrační testy s Dockerem – cesta tam a zase zpět

V textu již bylo několikrát nastíněno, že jednou z domén, kde nám Docker může být velice nápomocný, je testování. Ať už testování lokální nebo v CI.

Pokud bychom chtěli otestovat velkou škálu našich aplikací, museli bychom mít nainstalovány na testovacích strojích velké počty verzí MySQL databází, Elasticsearch, RabbitMQ, spoustu našich integračních nástrojů a mnoho dalšího. Pokud bychom provozovali takové množství aplikací, museli bychom vynaložit mnoho prostředků pro jejich údržbu. To ale nikdo z nás nechce.

I kdybychom měli možnost rozjet více odlišných verzí MySQL a dalších aplikací na jednom stroji, stále bychom museli testovacímu stroji podstrkovat SQL a jiná data potřebná pro integrační otestování. Proto jsme se začali u nás ve firmě věnovat způsobům, jak využít Docker právě pro CI testování. Docker nám nabízí mnoho různých cestiček, otázkou však je, která pro nás bude nejužitečnější a nejefektivnější.

Pro průzkum bojem jsme si vybrali projekt, který odpovídá naší standardní aplikaci. Jedná se o Java 8 aplikaci sestavenou Mavenem, která využívá databázi MySQL 5.6 a Elasticsearch 1.7.9 (finální setup je složitější, ale pro demostraci postačí).

Cilem průzkumu bylo spuštění JUnit testů v prostředí Jenkins s tím, že by se integrační testy pouštěly proti dockerizovaným nástrojům MySQL a Elasticsearch. Celé by to mělo pracovat tak hezky, že Jenkins udělá nějakou akci, na základě této akce se spustí kontejnery s MySQL a Elasticsearch, pomocí Mavenu se provedou JUnit testy, celé se to ukončí a stav kontejnerů se smaže. Další iterace, opět od začátku.

Bylo potřeba nainstalovat Jenkins, Docker, Docker compose a takové ty věci jako Java. To zde nebudu rozepisovat. Berme to tak, že prostředí máme připravené a můžeme nastavovat.

Cesta první

V ideální případě bychom chtěli, aby se všechny Docker kontejnery zapnuly ve stejnou chvíli. Po jejich zapnutí by se vypálila nějaká událost. Na základě události by se spustil JUnit test Mavenem a po tom, co by doběhl, by se kontejnery naráz vypnuly a jejich stav smazal.

Zapnutí všech kontejnerů naráz není vůbec žádný problém. Pro tento případ existuje nástroj docker-compose, kde je pomocí konfiguračního souboru docker-compose.yml možné nastavit celou “farmičku” kontejnerů. Tyto kontejnery pak snadno nahodíme pomocí:

docker-compose up

Jednoduché, ale má to první háček. Ono se to spustí, ale na popředí a pak čeká a čeká. Předání řízení Jenkinsu je tímto způsobem nemožné. Docker compose je možné spustit s přepínačem -d , který už podle písmena napovídá, že pustí kontejnery jako deamon. Jenže ten s Jenkinsem také moc nekomunikuje. A tak jediná možnost, jak tento první zádrhel obejít, je klasické *nixové & za příkazem. Tím se nám rozjedou na Jenkinsu kontejnery na pozadí, přesně jak chceme.

Nyní můžeme spustit testy Mavenem. A zde je druhý zádrhel. Co když se nám nestihnou spustit služby do doby, než se pustí test? Pak testy spadnou a celý postup bude kontraproduktivní. Jednoduchým bruteforce řešením by bylo přidat mezi spuštění kontejnerů a Mavenu shellovské:

sleep 10

Docker kontejnery a aplikace v nich naštěstí startují poměrně rychle, a tak 10s stačilo.

Po úspěšném dokončení testování Mavenem jsme vypnuli docker kontejnery a jejich stav vymazali:

docker-compose kill
docker-compose rm -f

Testy proběhly, my bychom měli výsledky a vše by bylo na správné cestě. Ale je v tom třetí a možná nejpodstatnější háček. Pokud spouštíme aplikaci v kontejneru tak, aby mohla aplikace spuštěná v systému, nebo Jenkinsu naslouchat na jejím portu, je potřeba port v kontejneru namapovat na port hosta. A v tomto případě máme problém. Nemohli bychom zároveň spustit dva testy aplikací, které jsou napojeny na stejnou MySQL na výchozím portu – localhost:3306.

Vyskytlo se tolik problémů, že tuto cestu nedoporučujeme využít.

Cesta druhá

Po zjištění problémů s mapováním portů jsme se rozhodli, že zkusíme dostat celý proces do Docker kontejnerů, a to včetně samotného testování Mavenem. Po zjištění, že už existuje image Mavenu na Docker Hubu, jsme ji vyzkoušeli. Kupodivu testy provedené touto image byly i poměrně rychlé a celkově se nám přístup líbil. Při testech na MacOS se zdálo, že je i tento způsob naprosto dokonalý.

Vytvořili jsme si konfiguraci Docker compose, dali do ní všechny tři kontejnery: MySQL, Elasticsearch, Maven a spustili akci. Kontejnery se nastartovaly, Maven začal testovat a po ukončení testu se všechny kontejnery samy vypnuly.

Nicméně se stále bavíme o Dockeru. Když jsme tento proces nasadili na Jenkins s Linuxem, tak nastal problém. Po dokončení testu nedošlo k samovolnému ukončení a docker stále měl řízení. Jenkins nemohl ukončit testování bez explicitního zavolání docker-compose kill.

Po malém průzkumu jsme zjistili, že se jedná o “fíčuru”, a že se kontejnery standardně neukončují. K tomu, aby došlo k ukončení kontejnerů po testech, se používá mnoho udělátek založených na aktivním čekání v cyklu s uspáním cca na 5s. Stále se musí testovat přes Docker procesy, zda kontejner s Mavenem žije a jakmile se ukončí, tak cyklus zavolá opět docker-compose kill.

Řešení je to jednoduché, ale myslím, že zbytečné. Navíc nám neřeší druhý problém z první cesty. A to, když se služby zapnou později. Musí existovat elegantnější cesta.

Cesta třetí a zatím finální

Po tom, co jsme několikrát narazili na limity technologie, jsme se rozhodli, jít cestou jinou.

Začátek je stále stejný. Máme docker-compose.yml s definicí MySQL a Elasticsearch kontejneru:

mysql:
 image: mysql:5.6.28
 environment:
   MYSQL_ROOT_PASSWORD: root
   MYSQL_DATABASE: portal_test
   MYSQL_USER: user
   MYSQL_PASSWORD: pass
 hostname: mysql
 container_name: mysql
 command:
   - --character-set-server=utf8
   - --collation-server=utf8_czech_ci
elasticsearch:
 image: elasticsearch:1.7.4
 hostname: elasticsearch
 container_name: elasticsearch

Kontejner startujeme Jenkinsem compose příkazem:

docker-compose up &

Jak je vidět, pouštíme ho na pozadí, aby neblokoval další volání jenkins. Teď by stačilo zavolat Maven a mohli bychom spustit testy. Ale oproti prvnímu případu nemáme v konfiguraci Docker compose mapované porty na hosta. To znamená, že se nemůžeme z hosta na nástroj v Docker kontejneru připojit. A proto je potřeba postit JUNit testy Mavenem také v kontejneru.

Každopádně, jak je vidět, tak oproti předchozímu způsobu nemáme Maven kontejner v compose konfiguraci zmíněn. Tento kontejner budeme spouštět manuálně.

docker run --rm --name docker --link mysql  --link elasticsearch -v "$PWD":/usr/src/mymaven -v "$PWD"/docker/:/root/.m2 \ 
-h docker -w /usr/src/mymaven maven:3-jdk-8 mvn clean test findbugs:findbugs -f java_web/pom.xml

Jak je z příkazu vidět, je zde použito linkování tohoto samostatného kontejneru na kontejnery spuštěné pomocí Docker compose. Tímto způsobem jsme schopni vidět na nástroje, které jsme již spustili.

Abych to vysvětlil. Docker si vytváří svoji interní síť a registruje do ní kontejnery. Pokud si nevytvoříme svoji síť a explicitně ji do konfigurace nedoplníme, použije se výchozí síť Dockeru. Každému kontejneru je přidělena IP a hostname. Hostname s container_name se buď generuje, nebo je možné jej explicitně specifikovat. Jakmile na nově spouštěném kontejneru specifikujeme propojení jiného kontejneru linkováním, zanese si informaci o IP a hostname linkovaného kontejneru do /etc/hosts.

Ve zkratce jsem schopen říct, že jsme eliminovali samostatným spuštěním kontejneru s Mavenem hned dva problémy. První eliminovaný probém je již zmíněné mapování portů na hostovi. Veškerá TCP/UDP konverzace probíhá uvnitř virtuální Docker sítě, a proto není potřeba publikovat žádný port ven z Docker infrastruktury. Druhým eliminovaným problémem je nepředávání řízení Jenkinsu po ukončení testování. Jakmile Maven v kontejneru skončí, kontejner se sám vypne a předá řízení opět Jenkinsu.

Následuje již zmíněné:

docker-compose kill
docker-compose rm -f

Stále ale ve vzduchu visí jeden problém. Co když se nestihnou spustit kontejnery z Docker compose dříve, než se budeme snažit nastartovat kontejner s Mavenem. V negativním scénáři, kdy se opravdu nepodaří spustit kontejnery, spuštění Maven kontejneru skončí s chybovým hlášením, že neexistují linkované kontejnery. Problém lze eliminovat tak, že po spuštění compose chvilku vyčkáme:

docker-compose up &
sleep 5

Stačí opravdu krátká doba. Zde nečekáme na to, až se nástroje uvnitř kontejneru spustí. Zde se čeká jen na start samostatného kontejneru. Do Docker sítě se zaregistruje až spuštěný kontejner a ten je pak následně linkovatelný.

Ale někdy potřebujeme vyčkat se spuštěním testů do doby, než se spustí i nástroje uvnitř kontejneru. Například nemůžeme pustit testy pracující s databází, pokud neběží MySQL. Zjistit, zda služba jede, je možné teoreticky v tuto chvíli jen z aktivního čekání v uspávaném cyklu. V cyklu je testována dostupnost TCP portu. Testuje se, zda port přijme spojení či ne. Celkově se cyklus chová jako bariéra. Dokud nejsou všechny podmínky na dostupnost splněny, tak se čeká. Po splnění obvykle proces skončí, aby mohl pokračovat proces jiný. Aby byly naše podmínky splněné, musí tento čekací algoritmus běžet uvnitř dalšího Docker kontejneru, aby měl dostupné linkované kontejnery spuštěné z compose.

Přesně toto jsme chtěli v našem testovacím procesu použít. Psát ale vlastní řešení nebo vymýšlet kolo, nebylo to pravé ořechové. Nakonec jsme narazili na aplikaci Dockerize, která splňovala přesně naše očekávání. Tato jednoduchá aplikace po splnění podmínek spustí jiný proces a skončí nebo pouze skončí.

Tuto aplikaci bylo potřeba spustit pro naše potřeby v kontejneru. Proto jsme si vytvořili vlastní image s aplikací dockerize.

Vybuildovat tuto image je velmi jednoduché:

docker buid -t lcir/dockerize .

Po sestavení image je možné ji použít ve vlastním testovacím procesu Jenkinse:

docker run --rm --name dockerize --link mysql --link elasticsearch \
lcir/dockerize dockerize -timeout 200s -wait tcp://mysql:3306 -wait http://elasticsearch:9200

Spouštíme dockerize v samostatném kontejneru s nalinkovanými kontejnery MySQL a ElasticSearch. Dockerize má čekat 200s, zda se něco stane. Pokud ne, dockerize se ukončí. Každopádně čekáme na TCP port 3306 na kontejner mysql a na port 9200 u kontejneru elasticsearch.

Jakmile jsou podmínky splněny, bariéra pustí zpracování dál. Nyní konkrétně už na JUnit Maven testování.

Pro úplnost je zde zmíněn celý build postup:

 docker buid -t lcir/dockerize .

 docker-compose up &
 sleep 5
 docker run --rm --name dockerize --link mysql --link elasticsearch \ 
 lcir/dockerize dockerize -timeout 200s -wait tcp://mysql:3306 -wait http://elasticsearch:9200

 docker run --rm --name docker --link mysql  --link elasticsearch -v "$PWD":/usr/src/mymaven -v "$PWD"/docker/:/root/.m2 \ 
 -h docker -w /usr/src/mymaven maven:3-jdk-8 mvn clean test findbugs:findbugs -f java_web/pom.xml
 
 docker-compose kill
 docker-compose rm -f

Závěrem

Myslím si, že až se vyladí pár problematických konstrukcí v Dockeru, zlepší se podpora pro Microsoft a Apple, bude Docker hrát velice důležitou roli v další historii IT. Uvidíme, kam až tento nadějný nástroj jeho tvůrci dostanou.

My se budeme nadále snažit pomocí Dockeru zvýšit efektivitu naší práce. Postupně budeme do našich pracovních postupů stále více Docker integrovat. Možností využití zde bylo popsáno několik. Určitě v budoucnu napíšu, jak se nám to daří.

 

Pracuji ve firmě FG Forrest, a.s. jako Java vývojář, ve volných chvílích organizuji v Hradci Králové vývojářské akce.

Věděli jste, že nám můžete zasílat zprávičky? (Jen pro přihlášené.)

Komentáře: 5

Přehled komentářů

boss
Lukáš Cír Re:
boss Re:
Lukáš Cír Re:
Lukáš Cír Re:
Zdroj: https://www.zdrojak.cz/?p=17566