X spotkanie SPINu w Krakowie – 22.11 Oswajanie Kerberosa, czyli dwunasta praca Heraklesa
Dec 01

Hudson

Szczypta teorii

W żadnej poważnej publikacji nie może oczywiście zabraknąć części czysto teoretycznej! Pomimo faktu, że ta publikacja do tak zaszczytnego grona nie pretenduje, to z przykrością stwierdzam, że i tutaj teoretyczny wstęp znaleźć się niestety musi. Osoby niezainteresowane proszone są o przesunięcie paska przewijania odrobinę niżej.

Z roku na rok wdrażane systemy informatyczne stają się funkcjonalnie bardziej złożone, terminy ich oddania krótsze, a stawiane im wymagania jakościowe surowsze. Sprostanie ciągle rosnącym oczekiwaniom klientów biznesowych wymaga od firm rynku IT stosowania coraz większej ilości narzędzi formalizujących i automatyzujących proces wytwarzania aplikacji.

Już w tym momencie do grona narzędzi uważanych za niezbędne w cyklu wytwarzania oprogramowania zalicza się, i to z coraz większą stanowczością, aplikacje wspomagające proces ciągłej integracji (ang. Continuous Integration).

Uważam, że koncepcja Continuous Integration (dalej: CI) jest szczególnym przypadkiem wzorca projektowego i jako taka obecna była w informatyce w niesformalizowanej formie już od dawna. Jednak jej znacząca popularyzacja miała miejsce dopiero w okresie upowszechnienia się metodyki Extreme Programming. Z pewnością ogromny wpływ na taki stan rzeczy miały publikacje Martina Fowler‘a oraz Kenta Beck‘a, które nadały CI realne kształty i obudziły dyskusję na ten temat w światku informatycznym. Przedstawiona idea odbiła się dużym echem i w wielu projektach zaczęto wdrażać przedstawioną koncepcję.

Główna myśl stojącą za CI to wymuszenie zbudowania całego projektu w wyniku każdej najmniejszej nawet zmiany. Proces budowania powinien obejmować między innymi kompilację kodu źródłowego, sprawdzenie jego jakości przy wykorzystaniu różnych kryteriów (np. checkstyle‘a), wykonanie testów jednostkowych (np. JUnit) oraz weryfikacja stopnia w jaki pokrywają one kod źródłowy (np. Emma), wytworzenie artefaktów (w tym np. pliki war, dokumentacja). Wszystkie te działania koordynowane mogą być oczywiście przy pomocy Apache Ant, Apache Maven, czy też innego narzędzia przeznaczonego do budowania, chociaż dwa wymienione jako pierwsze wydają się być w tym momencie bezkonkurencyjne.

Widać więc zatem, że CI nie stara się wymyślać koła na nowo, a jedynie stanowi kolejną warstwę abstrakcji ponad szeroko już wykorzystywanymi narzędziami.

Zalety i wady

Podstawowe zalety CI wymieniane na większości stron internetowych związanych z tematyką, to:

  • możliwość dokładnego określenia która z wprowadzonych zmian spowodowała dezintegrację rozwijanego systemu poprzez określenie w którym momencie któryś z testów jednostkowych przestał poprawnie przechodzić
  • problemy związane z integracją są wykrywane i naprawiane na bieżąco
  • natychmiastowa informacja na temat popsutego kodu
  • natychmiastowe wykonywanie testów jednostkowych
  • dostępność najnowszej wersji zbudowanej aplikacji bez konieczności jej ręcznego budowania
  • możliwość śledzenia w czasie wartości różnych wskaźników opisujących różne parametry wytwarzanej aplikacji (np. stopień pokrycia kodu testami jednostkowymi)
  • programiści zmobilizowani są do tworzenia lepszych rozwiązań; są bardziej świadomi, że każda wprowadzana przez nich zmiana może zostać poddana obiektywnej ocenie

Obok w/w zalet istnieją zapewne inne pomniejsze, które każdy z nas na swój sposób może w CI odnaleźć. Jakie są zatem minusy wdrożenia procesu ciągłej integracji? O wadach CI Internet dosyć głośno milczy i mnie również trudno jest znaleźć przynajmniej kilka wad, które mogłyby mieć generalny i warty wspominania charakter. Nie oznacza to jednak, że CI nie może nam przysporzyć dodatkowych problemów. Analizę potencjalnych minusów należy jednak przeprowadzić indywidualnie przy okazji wyboru konkretnego modelu realizacji CI. W większości jednak projektów używanie CI będzie sprawiać mniejsze problemy, niż te, które mogłyby wystąpić w wyniku jego nieużywania.

Manual vs. Automated

Manual Continuous Integration

Najprostszym możliwym sposobem wdrożenia CI w projekcie jest przerzucenie odpowiedzialności za jego stosowanie na programistów. Najprostszym i zarazem chyba najgorszym z możliwych. Mogłoby to polegać na zobowiązaniu osób związanych z procesem wytwarzania aplikacji do każdorazowego wykonywania serii żmudnych i mechanicznych czynności po każdej pojedynczej zmianie wprowadzonej do repozytorium. Niestety takie podejście ma bardzo wiele wad i wydaje się, że może się jedynie sprawdzać w bardzo małych projektach.

Podstawowe problemy związane z manualnym stosowaniem CI, które jestem w stanie w tym momencie wymienić, to:

  • developerzy muszą poświęcić dodatkowy czas na procedury związane z CI
  • występuje możliwość niedbałego podejścia developerów do stosowania CI
  • obniżenie kreatywności developerów poprzez wykonywanie żmudnych i powtarzalnych procedur
  • niedeterministyczne opóźnienia w publikacji artefaktów związane z koniecznością manualnego wykonania wszystkich procedur

Wymienione powyżej problemy, to jedynie niektóre z którymi możemy mieć do czynienia przy ręcznym stosowaniu koncepcji CI. Przedstawiona lista stanowi jednak dla mnie wystarczający powód, by CI w miarę możliwości starać się stosować jedynie w zautomatyzowanej formie. Tym bardziej, że proces przeprowadzania ciągłej integracji wytwarzanego projektu można bez większych przeszkód opisać w prostej sformalizowanej postaci. Umożliwi to jego automatyczne wykonywanie w wyniku konkretnego zdarzenia zachodzącego w naszym systemie (w klasycznej i najczęściej stosowanej postaci będzie to wprowadzenie przez developera zmian do kodu źródłowego projektu).

Automated Continuous Integration

Na szczęście, by wyrwać nas z marazmu związanego z ręcznym stosowaniem CI, przychodzą nam wyspecjalizowane narzędzia automatyzujące ten proces. Jeżeli nasze zakłopotanie nie mija, to tylko dlatego, że wybór odpowiedniego narzędzia, ze względu na ich mnogość, nie jest wcale prosty. Dostępnych jest przynajmniej kilkadziesiąt produktów tego rodzaju. Wybieranie aplikacji spełniającej nasze oczekiwania (w sposób możliwie najbardziej kompletny) powinno obejmować rozważenie licencji produktu, zastanowienie się nad czynnościami, które będziemy chcieli włączyć do procesu CI oraz z jakimi narzędziami chcielibyśmy, aby wybrana implementacja ACI współpracowała. Poza tym, co zostało wymienione, powinniśmy kierować się jak zawsze zdroworozsądkowymi kryteriami – podobnie jak przy doborze innych narzędzi.

W miarę kompletną listę narzędzi do automatyzacji procesu ciągłej integracji można znaleźć tutaj.

Gram praktyki

Kiedy już poznaliśmy teoretyczne podstawy CI najwyższy czas, aby przejść do jego praktycznego wykorzystania. Zaczniemy od określenia szczegółowych wymagań dotyczących modelu procesu ciągłej integracji, który chcielibyśmy zaimplementować w naszym przykładowym projekcie. By tego jednak dokonać wcześniej postaram się opisać w kilku słowach środowisko wytwórcze powiązane z naszym przykładowym projektem nazwanym przeze mnie Validator.

Elementy środowiska wytwórczego dla projektu Validator:

  • Wiodący język programowania: Java
  • Repozytorium: Subversion
  • Narzędzie automatyzujące proces budowania: Apache Ant
  • Silnik testów jednostkowych: JUnit
  • Narzędzie do weryfikacji pokrycia kodu: Emma

A teraz cele, które chcielibyśmy osiągnąć dzięki zastosowaniu Automated Continuous Integration. Każde wprowadzenie zmiany do repozytorium powinno skutkować:

  • kompilacją kodu źródłowego
  • wykonaniem wszystkich testów jednostkowych (wraz z utrwaleniem rezultatów)
  • obliczeniem stopnia pokrycia kodu testami na poziomie klas, metod oraz linii (wraz z utrwaleniem rezultatów)
  • sprawdzeniem jakości kodu przy użyciu checkstyle (wraz z utrwaleniem rezultatów)
  • zbudowaniem biblioteki w postaci pliku jar oraz udostępnieniu go do pobrania
  • wysłaniem pod wskazany adres informacji w postaci maila o niepowodzeniu na jakimkolwiek etapie procesu

Jak łatwo można zauważyć jest to bardzo prosty przykład procesu ciągłej integracji – można wręcz rzec, że szkoleniowy. W przypadku prawdziwych projektów prawdopodobnie będziemy mieli do czynienia z wielokrotnie bardziej złożonymi mechanizmami. Nie należy się jednak tym przerażać, gdyż w większości sytuacji dodatkowa praca, jaką należy wykonać jest związana, nie z samym narzędziem ACI, ale z narzędziem służącym do budowy aplikacji (np. Apache Ant).

Hudson

Wpisując w Google hudson można by pomyśleć, że Hudson, to rzeka. Nic bardziej mylnego ;) Hudson nie jest może najbardziej popularnym narzędziem ACI, ale z pewnością jest projektem, który bardzo dynamicznie się w ostatnim czasie rozwija i posiada podatną na rozszerzalność architekturę. Istnieje wiele wtyczek rozszerzających funkcjonalność Hudson‘a o dodatkowe możliwości, a i tak ciągle pojawiają się nowe, a już istniejące są rozwijane. Widać, że projekt żyje – wróży mu to dobrą przyszłość. Hudson ma także jeszcze jedną niezaprzeczalną zaletę – osobiście bardzo lubię go używać i nie ukrywam, że mam nadzieję, że ten artykuł w jakiś, chociaż nieznaczny, sposób wpłynie na jego szybszą popularyzację.

Instalacja

Instalacja Hudson‘a jest wyjątkowo prosta. Wystarczy posiadać kontener servlet‘ów – może to być na przykład Apache Tomcat. W przypadku Tomcat‘a wystarczy jedynie przekopiować ściągnięty ze strony producenta plik hudson.war do katalogu webapps. Następnie uruchamiamy Tomcat‘a i to już wszystko! Przy standardowej konfiguracji Hudson powinien być dostępny pod adresem http://localhost:8080/hudson.

Konfiguracja Hudson‘a odbywa się poprzez Manage Hudson/System Configuration dostępne z głównej stronie. Minimalne ustawienia, jakie należy wprowadzić, to między innymi:

  • lokalizacja instalacji JDK
  • lokalizacja instalacji Ant/Maven
  • konfiguracja serwera SMTP z którego będzie korzystał Hudson

Dodatkowo, aby umożliwić spełnienie wymagań, które sobie wcześniej postawiliśmy konieczne jest jeszcze ściągnięcie ze strony odpowiednich wtyczek i ich zainstalowanie:

  • Emma – posłuży nam do śledzenia postępu pokrycia kodu testami (pomiędzy kolejnymi build‘ami)
  • Violations – posłuży nam do obrazowania zastrzeżeń, jakie checkstyle ma w stosunku do naszego kodu

Instalacji plugin‘ów należy dokonać poprzez opcję Manage Hudson/Manage Plugins. Po zainstalowaniu wtyczek należy zrestartować serwer aplikacji. W tym momencie powinniśmy posiadać już Hudson‘a w postaci pozwalającej nam na osiągnięcie wyżej wskazanych celów. Teraz czeka nas jeszcze trochę pracy poza ACI związanej z czynnościami, które należy wykonać nawet, jeżeliby projekt nie korzystał z dobrodziejstw CI.

Skrypt budujący

Poniżej pozwoliłem sobie zamieścić używany w projekcie skrypt budujący Apache Ant. Jak łatwo można zauważyć jest to zwykły skrypt budujący i nic nie wskazuje na to, że był on przygotowany w celu późniejszej współpracy z Hudson‘em. Nie posiada on także żadnych magicznych powiązań z systemem ACI. Nic zatem nie stoi na przeszkodzie by wywołać go bezpośrednio za pomocą Ant‘a. Kompletne źródła projektu Validator można ściągnąć stąd.

<?xml version="1.0"?>
<project name="Validator" default="build_jar">

  <property name="main.source.dir" location="src" />
  <property name="test.source.dir" location="test" />
  <property name="lib.dir" location="lib" />
  <property name="build.dir" location="build" />
  <property name="dist.dir" location="${build.dir}/dist" />
  <property name="main.source.build.dir" location="${build.dir}/classes/source" />
  <property name="main.source.debug.dir" location="${build.dir}/classes/source_debug" />
  <property name="main.source.instrumented.dir" location="${build.dir}/classes/source_instrumented" />
  <property name="test.source.build.dir" location="${build.dir}/classes/test" />
  <property name="test.result.dir" location="${build.dir}/test" />
  <property name="violations.result.dir" location="${build.dir}/violations" />
  <property name="coverage.result.dir" location="${build.dir}/coverage" />
  <property name="code.coverage.metadata.file" location="${coverage.result.dir}/coverage.em" />
  <property name="code.coverage.xml.file" location="${coverage.result.dir}/coverage.xml" />
  <property name="code.coverage.html.file" location="${coverage.result.dir}/coverage.html" />
  <property name="checkstyle.result.file" location="${violations.result.dir}/checkstyle.xml" />
  <property name="main.jar.file" value="validator.jar" />

  <taskdef name="emma" classname="com.vladium.emma.emmaTask">
    <classpath>
      <fileset dir="${lib.dir}">
        <include name="emma_ant.jar" />
        <include name="emma.jar" />
      </fileset>
    </classpath>
  </taskdef>

  <taskdef resource="checkstyletask.properties" classpath="${lib.dir}/checkstyle-all-4.3.jar" />

  <path id="test.build.classpath">
    <pathelement location="${main.source.build.dir}" />
    <fileset dir="${lib.dir}">
      <include name="junit.jar" />
    </fileset>
  </path>

  <path id="test.runtime.classpath">
    <pathelement location="${main.source.instrumented.dir}/classes" />
    <pathelement location="${test.source.build.dir}" />
    <fileset dir="${lib.dir}">
      <include name="junit.jar" />
      <include name="emma.jar" />
    </fileset>
  </path>

  <target name="clean">
    <delete dir="${build.dir}" />
  </target>

  <target name="compile_main">
    <mkdir dir="${main.source.build.dir}" />
    <javac srcdir="${main.source.dir}" destdir="${main.source.build.dir}" />
  </target>

  <target name="compile_main_debug">
    <mkdir dir="${main.source.debug.dir}" />
    <javac srcdir="${main.source.dir}" destdir="${main.source.debug.dir}" debug="true" />
  </target>

  <target name="compile_test">
    <mkdir dir="${test.source.build.dir}" />
    <javac srcdir="${test.source.dir}" destdir="${test.source.build.dir}">
      <classpath refid="test.build.classpath" />
    </javac>
  </target>

  <target name="run_test" depends="compile_main,compile_main_debug,compile_test">
    <mkdir dir="${test.result.dir}" />
    <delete dir="${main.source.instrumented.dir}" />
    <mkdir dir="${main.source.instrumented.dir}" />
    <emma>
      <instr outdir="${main.source.instrumented.dir}" mode="fullcopy" merge="false" metadatafile="${code.coverage.metadata.file}">
        <instrpath>
          <pathelement location="${main.source.debug.dir}" />
        </instrpath>
      </instr>
    </emma>
    <junit fork="yes">
      <formatter type="xml" />
      <classpath refid="test.runtime.classpath" />
      <sysproperty key="emma.coverage.out.file" value="${code.coverage.metadata.file}" />
      <sysproperty key="emma.coverage.out.merge" value="true" />
      <batchtest todir="${test.result.dir}">
        <fileset dir="${test.source.dir}" />
      </batchtest>
    </junit>
    <emma>
      <report sourcepath="${main.source.dir}">
        <infileset file="${code.coverage.metadata.file}" />
        <xml outfile="${code.coverage.xml.file}" />
        <html outfile="${code.coverage.html.file}" />
      </report>
    </emma>
  </target>

  <target name="violations">
    <mkdir dir="${violations.result.dir}" />
    <checkstyle config="${lib.dir}/sun_checks_eclipse.xml">
      <fileset dir="${main.source.dir}" includes="**/*.java" />
      <formatter type="xml" toFile="${checkstyle.result.file}" />
    </checkstyle>
  </target>

  <target name="build_jar" depends="run_test,violations">
    <mkdir dir="${dist.dir}" />
    <jar basedir="${main.source.build.dir}" destfile="${dist.dir}/${main.jar.file}" />
  </target>

</project>

Z punktu widzenia Hudson‘a i konfiguracji, której musimy dokonać istotne będą jedynie wymienione poniżej elementy:

  • położenie pliku validator.jar będącego efektem końcowym procesu budowania, który chcemy udostępnić jako artefakt
  • położenie pliku XML zawierającego wyniki wykonania testów jednostkowych
  • położenie pliku XML zawierającego informacje na temat pokrycia kodu testami jednostkowymi

Tworzenie nowego zadania Hudson‘a

Powoli zbliżamy się do meritum. Mając już gotowy projekt wraz z działającym skryptem budującym nie pozostało nam już wiele do zrobienia, by objąć go procesem ciągłej integracji. Właściwie dzieli nas od finału już tylko jeden krok, którym będzie utworzenie nowego zadania w Hudson‘ie.

Dokonanie tego nie będzie na szczęście niczym skomplikowanym:

  1. z głównego panelu sterowania Hudson wybieramy opcję New Job
  2. określamy nazwę dla tworzonego zadania
  3. wybieramy free-style jako rodzaj projektu i przechodzimy do następnego ekranu, gdzie wpiszemy pozostałe szczegóły
  4. wybieramy system kontroli wersji z którego będzie korzystał Hudson (w naszym przypadku będzie to Subversion)
  5. jako warunek wywołania procesu integracji (Build Triggers) wybieramy cykliczne odpytywanie repozytorium (Poll SCM)
  6. w harmonogramie wpisujemy łańcuch znaków zgodny z formatem crond (np. */2 * * * * – odpytywanie co 2 minuty)
  7. następnie definiujemy, co właściwie będzie oznaczać dla Hudson‘a zbudowanie aplikacji – w naszym przypadku chcemy, aby było to wywołanie przy pomocy Ant‘a domyślnego celu zdefiniowanego w skrypcie budowania (zaznaczamy Invoke Ant)
  8. przyszedł czas na określenie czynności, które zostaną wykonane po zakończeniu budowania aplikacji – zgodnie z wcześniejszymi ustaleniami będzie interesowało nas:
    1. zachowanie artefaktów (validator.jar) z procesu budowania (Archive the artifacts)
    2. opublikowanie wyników testów jednostkowych (Publish JUnit test result report)
    3. opublikowanie wyników pokrycia kodu testami (Record Emma coverage report)
    4. opublikowanie wyników sprawdzenia jakości kodu przy użyciu checkstyle‘a (Report violations)
    5. wysłanie informacji o integracji, która się nie powiodła (E-mail Notification)

Tym razem to już naprawdę wszystko! Po zatwierdzeniu konfiguracji (jeżeli wszystko zrobiłeś dobrze ;)), to możesz się już cieszyć działającym procesem ciągłej integracji.

Od tej pory możesz zacząć regularną pracę zapominając zupełnie o istnieniu Hudson‘a i sięgać do niego jedynie wtedy kiedy uznasz to za przydatne. A sytuacji takich może być co najmniej kilka:

  • chęć uzyskania zbudowanej wersji aplikacji z konkretnego build‘a
  • analiza zmienności w czasie różnych parametrów opisujących aplikację w celu określenia trendów (np. procentowe pokrycie testami kodu źródłowego)
  • zatwierdzenie zmian w repozytorium, które spowodują niebudowanie się aplikacji spowoduje wysłanie maila na wskazany adres

Podsumowanie

Rozważany przez nas przykład był raczej przykładem trywialnym. Z tego też powodu może się wydawać, że Hudson nie pełni w nim znaczącej roli. Muszę się zgodzić z tą opinią – czym jednak system staje się większy i bardziej skomplikowany, tym ACI staje się bardziej niezbędny.

Miałem przyjemność pracować nad ogromnym systemem z wieloma podprojektami (repozytorium wielkości około 3GB), gdzie Hudson stanowił bardzo ważne ogniwo w procesie wytwarzania. Wydaje się więc, że jeżeli był w stanie poradzić sobie w takich warunkach, to dla większości standardowych zastosowań powinien być odpowiedni. Osobiście używam Hudson‘a, gdyż pozostając bardzo intuicyjny oraz łatwy w konfiguracji dostarcza możliwości na zupełnie wystarczającym dla mnie poziomie zaawansowania.

Mam nadzieję, że mimo ograniczonych środków przekazu udało mi się przekonać Cię do wypróbowania Hudson‘a na własnej skórze. Świadom jestem niestety, że artykuł ten to zaledwie czubek góry lodowej zarówno jeżeli chodzi o CI, jak i o samegoHudson‘a.

Bibliografia

  1. http://en.wikipedia.org/wiki/Continuous_Integration
  2. http://www.extremeprogramming.org/
  3. http://hudson.gotdns.com/wiki/display/HUDSON/Home
  4. http://martinfowler.com/
Podziel się z innymi:
  • Wykop
  • Digg
  • del.icio.us
  • StumbleUpon
  • Slashdot

3 Responses to “Hudson jako przykład implementacji koncepcji Automated Continuous Integration”

  1. Ris Says:

    Bardzo ciekawy i pomocny artykuł. Dobrze ujęty temat.

  2. woro Says:

    Rzeczowo i na temat. Do wad lub zalet CI (w zależności od punktu siedzenia) zaliczyłbym jeszcze obowiązek naprawy przez developera szkód które jego commit wyrządził (np. powodujące wyłożenie testów) :)

  3. Michał Mally Says:

    Może to nie najlepsze miejsce na zamieszczanie rozwiązania mojego problemu, ale spróbujmy.

    Kilka ostatnich dni przemęczyłem się z moją instalacją Hudsona uruchomioną w Tomcacie na Gentoo… problem wydaje się być nie związany z samym Hudsonem… a nawet nie z Tomcatem… ale właśnie ze skryptami startowymi Gentoo. W moim przypadku problem polegał na tym, że instancja mavena uruchamiana przez Hudsona nie brała pod uwagę globalnej zmiennej środowiskowej MAVEN_OPTS i wszystkie buildy wysypywały się ze względu na brak pamięci. Po wielu próbach ustawienia tej zmiennej we wszelakich możliwych miejscach tak, aby była ona widoczna dla Tomcata… a co za tym idzie także dla Hudsona… natrafilem na plik /etc/init.d/tomcat-6.

    Aby zwiększyć stos dodałem do wywołania start_stop_daemon paramentr ‘–env MAVEN_OPTS=”-Xmx512m”‘. To całkowicie i finalnie rozwiązało mój problem :)

Leave a Reply

Security Code: