Od dłuższego czasu interesowało mnie Continuous Integration w wykonaniu Attlasiana i jego serwera Bamboo. Do tego doszedł popularny ostatnio Docker, który wydawał mi się bardzo dobrym rozwiązaniem do szybkiego stawiania środowiska potrzebnego do zbudowania aplikacji.
Postanowiłem zatem zrobić prosty proof of concept – żeby zobaczyć jak to wygląda w praktyce.
Nie będę tutaj opisywał jak zainstalować Bamboo czy Docker i docker-compose czyjak dodać projekt w Bamboo – odsyłam do znakomitych dokumentacji. Skupię się tylko na problemie z tytułu posta.
Scenariusz
Serwery – Bamboo i Docker
Mamy dwa serwery – „bamboo” oraz „build1”.
Przeznaczeniem serwera „bamboo” jest hostowanie Bamboo i tylko tyle. „build1” jest zdalnym agentem dla „bamboo”, na nim także zainstalowany jest Docker. Na „build1” będzie budowana nasza aplikacja testowa – w dedykowanym dla niej kontenerze Dockera.
Bamboo umożliwia także odpalenie swojego agenta na tej samej maszynie na której się znajduje (tzw. „lokalny agent”), ale poza bardzo prostymi przypadkami, moim zdaniem, to proszenie się o kłopoty – środowiska powinny być odseparowane – Bamboo i tak ma nad nimi kontrolę właśnie dzięki zdalnym agentom.
Ja swoje środowisko postawiłem na dwóch maszynach wirtualnych przy użyciu Vagranta.
Aplikacja testowa
Aplikacją testową jest prosty projekt z PHP (framework Silex) – tylko, żeby pokazać użycie Composera i testów jednostkowych.
Projekt testowy hostuję na repozytorium Git na BitBucket.com. Jest ono spięty z Bamboo – odsyłam do dokumentacji jak to zrobić.
Struktura plików w projekcie jest następująca:
W katalogu „docker” znajdują się skrypty potrzebne do zbudowania środowiska na Dockerze (oraz skrypt do uruchomienia PHPUnit – ale o tym za chwilę), a w katalogu „src” właściwy projekt.
W src/web/index.php mamy instancję klasy libs/Message/Message.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php require_once __DIR__.'/../vendor/autoload.php'; require_once __DIR__.'/../autoload.php'; $app = new Silex\Application(); $app->get('/hello/{name}', function ($name) use ($app) { $message = new \Message\Message(); var_dump($message->show('rafal')); return 'Hello '.$app->escape($name); }); $app->run(); |
1 2 3 4 5 6 7 8 9 10 11 12 |
<?php namespace Message; class Message { public function show(string $name) { return !empty($name); } } |
którą przetestujemy w src/tests/libs/MessageTest.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
<?php class MessageTest extends PHPUnit_Framework_TestCase { public function testNotEmpty() { $message = new Message\Message(); $this->assertTrue($message->show('rafal')); } public function testFailNotEmpty() { $message = new Message\Message(); $this->assertFalse($message->show('')); } } |
Tak jak pisałem – to jest maksymalnie prosty projekcik. 😉
Akcja właściwa
Zakładam, że mamy w Bamboo projekt „bamboo_test”.
Tworzymy w projekcie nowy plan:
Drugi krok pozostawiamy na razie bez zmian:
Następnie wchodzimy w edycję naszego nowego planu (Build -> All Build Plans i klikamy w edycję). Będzie nas interesować zakładka „Stages”:
Wybieramy „Default job”. W zakładce Tasks ustawimy poszczególne zadania planu budowania.
Poniższy screen przedstawia już cały mój plan, omówię konfigurację poszczególnych zadań:
Pierwsze zadanie, czyli „Source Code Checkout” – już powinniśmy mieć (było zasugerowane podczas tworzenia planu). Pierwszy etap to pobranie kodu projektu z repozytorium.
Ja tylko dodatkowo włączyłem „Force Clean Build”, żeby podczas testowania niniejszego scenariusza, być pewnym, że wszystko od podstaw się buduje dobrze.
Kolejnym etapem jest postawienie naszego kontenera Dockera w którym będzie uruchomiona testowana aplikacja. Tak jak na poniższym obrazku:
Dodajemy, więc poprzez „Add task”, komendę („command”). Będzie brakowało pliku wykonywalnego („executable”) docker-compose, więc dodajemy poprzez „Add exectuable”.
docker-compose będzie pracował na serwerze „build1” w podkatalogu „docker”, zatem tak ustawiamy w tym zadaniu. Plik „docker/docker-compose.yml” wygląda u mnie następująco:
1 2 3 4 5 6 7 8 9 10 11 |
version: '2' services: web1: build: context: . dockerfile: Dockerfile-web1 volumes: - "../src:/var/www/html" ports: - "32001:80" |
Mam, jak widać jeden kontener („web1”), który jest zdefiniowany w pliku „docker/Dockerfile-web1”. Jest to kontener z PHP 7 i Composerem:
1 2 3 4 5 6 |
FROM php:7.1.0RC4-apache RUN apt-get update && apt-get -y install git unzip; \ curl -S https://getcomposer.org/installer | php && \ mv composer.phar /usr/local/bin/composer; \ docker-php-ext-install -j$(nproc) bcmath; \ pecl install xdebug && echo "zend_extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/xdebug.so" > /usr/local/etc/php/php.ini |
Kolejne zadanie to instalacja zależności projektu za pomocą Composera:
Kolejna dodana komenda nam to załatwi. Za pomocą „docker-compose exec” uruchami ona Composera na kontenerze. Cały czas pamiętamy, żeby wskazać podkatalog roboczy „docker”.
Kolejne dwa zadania odnoszą się do uruchomienia testów jednostkowych. Tutaj parę dodatkowych szczegółów. Jako, że Bamboo cały czas „myśli”, że PHPUnit jest na serwerze „build1”, a nie kontenerze Docker, napisałem skrypt bashowy („docker/phpunit-docker.sh), który opakowuje wywołanie PHPUnit przez docker-compose:
1 2 3 4 5 |
#!/usr/bin/env bash docker-compose exec -T web1 vendor/phpunit/phpunit/phpunit $@ echo 'OK' |
Ułatwiło mi to przekazywanie argumentów do PHPUnit przez zadanie Bamboo „PHPUnit”.
Dlaczego tam jest „echo 'ok'”? Bamboo nie uruchamiał interpertacji wyników testów w przypadku, gdy któryś z testów jednostkowych nie przechodził – w logach miałem, że brakuje „OK”, więc dopisałem. Zapewne da się to sensowniej rozwiązać, ale wystarczyło na potrzeby proof of concept.
Zatem wracjąc do naszych zadań.
Kolejne zadanie to ustawienie praw do uruchomienia skryptowi „docker/phpunit-docker.sh”. Można użyć do tego zadania typu „script”:
Następnie dodajemy zadanie typu „PHPUnit” w celu odpalenia testów jednostkowych:
Generowanie wyników testów obsłużyłem w polu „arguments”, gdyż miałem problemy ze ścieżkami, jeśli próbowałem korzystać z „natywnych” funkcji zadania „PHPUnit”.
Jak widać na zrzucie ekranu – generuję wynik testów w postaci pliku xml oraz raportu z pokrycia testami. Wygenerowane pliki będą potrzebne w kolejnych etapach budowania.
Kolejny etap to interpretacja wyników testów. Dodajemy, więc zadanie typu „JUnit Parser” (nie udało mi się obsłużyć parsowania wyników bezpośrednio w zadaniu „PHPUnit”):
Ostatnie dwa zadania w sekcji „final” to sprzątanie:
Pozostało jeszcze ustawienie interpertacji raportu pokrycia kodu testami, robimy to w zakładce „Miscellaneous”:
W ten sposób mamy konfigurację budowania gotową. Bamboo ładnie prezentuje wyniki testów czy pokrycie kodu testami:
Podsumowanie
Przedstawiłem prosty przykład użycia Dockera w procesie budowania przy użyciu serwera Bamboo.
Ogromną zaletą tego rozwiązania jest pewność, że aplikacja będzie budowana w identycznym środowisku w jakim była tworzona.