Itt jársz most: Kezdőlap > Alkalmazásfejlesztés > Continuous Integration a cloudban - CircleCI build plan beállítása

Szűrő megjelenítése

Continuous Integration a cloudban - CircleCI build plan beállítása

A CircleCI egyike azon, a GitHub-bal integrált CI megoldásoknak, melyekkel könnyedén leválthatjuk jelenlegi, "on-premise" CI megoldásunkat. Jómagam Jenkins build pipeline-ról tértem át - a migrálás gördülékenyen és könnyedén ment többnyire, bár voltak buktatói, már csak amiatt is, hogy némiképp más gondolkodásmódot képvisel a CircleCI. Egyrészt a build environment nem közvetlenül hozzáférhető, illetve a build plan tesztelése sem olyan könnyű, mint például a Jenkins használata során. Voltak ezentúl további kisebb-nagyobb megoldásra váró problémák, akadályok, melyekre az alábbi cikkben részletesen szeretnék kitérni.

Előnyök és hátrányok

Az első tény, amire itt kitérnék, az a viszonylag még újnak számító szemléletmód, miszerint alkalmazásaink buildjeit is érdemes containerekben futtatni. Az alkalmazások terén ez a szemléletmód már egyáltalán nem újdonság, már évekkel ezelőtt amolyan ipari standarddá váltak a container alapú virtualizációs megoldások, mint például a Docker. Vitathatatlan előnye a containerizációnak, hogy az alkalmazás függetlenedik az őt futtató környezettől, így elkerülve az operációs rendszer, a telepített futtatókörnyezet vagy akár a hardveres eltérések okozta kompatibilitási problémákat. A build pipeline-ok containerizációja egy újabb szintet képvisel ebben a szemléletmódban, mivel így már a fordítás, tesztelés és a deployment is függetlenedik az őket végrehajtó környezettől - az irányítást így teljesen a fejlesztő kezébe adva. Ez egyébként egy másik népszerű trend, a build pipeline-t leíró konfiguráció alkalmazással együtt való csomagolásának eredménye, illetve mivel a konfiguráció absztrakt és verziókezelhető formában kell ilyenkor megjelenjen, a Code as Configuration szemlélet is komoly hatással van az azt támogató rendszerek fejlődésére.

Bár egyértelmű előnye a fentebb taglaltaknak a build pipeline teljes felügyeletének lehetősége, ugyanakkor valamelyest hátrányként tekinthető az erős DevOps szükség megjelenése, tehát az alkalmazásfejlesztők kénytelenek tisztában lenni azzal, hogyan működik egy CI/CD pipeline, ismerniük kell a pipeline működtetésében résztvevő komponenseket, illetve az adott CI/CD rendszer konfigurációs API-át is magabiztosan ismerniük kell. Bár semmiképp sem elhanyagolható mennyiségű tudás szükségszerű megszerzéséről beszélünk, alapvetően az iránnyal nem látok problémát, hiszen sok esetben a CI/CD pipeline-t üzemeltető külső személyek/csapatok semmit nem tudnak az alkalmazásunkról, így az sem várható el tőlük, hogy hatékonyan felügyeljék a buildek épségét, tartsák karban azokat. Cikkemben ezzel talán kicsit ingoványos talajra eveznék, éppen ezért visszakanyarodnék most a témához, viszont mindenképp kíváncsi volnék a véleményeitekre - várom őket kommentben!

Visszatérve tehát a pro-khoz és contra-khoz, véleményem szerint a CircleCI-hoz hasonló CI/CD rendszerek használatával nagy teher esik le az üzemeltetők válláról, lévén külső, saját üzemeltetésű CI/CD rendszerek fenntartása ezzel szükségtelenné válik. Persze nem elhanyagolható tény a rendszer használatának megtanulása és az esetleg már létező rendszer migrálása, mely bőséges mennyiségű időt és erőforrást igényel. További előny az ehhez hasonló rendszereknek a verziókezelő rendszerrel való integrálása, bár ez többé-kevésbé könnyen megoldható az on-premise megoldásokban is.

Kisebb, főképp az open-source projektek fenntartói számára jó hír, hogy a CircleCI rendszere ingyenesen használható, bár a cikk írásakor csak 1 build agent használható az ingyenes planben. Planek és az azokhoz hozzáférő felhasználók számában nem, de build futtatási időben van korlátozás az ingyenes plan esetén, ez havi 1000 percet jelent (vagy 10.000 kreditet, ahogyan a rendszer maga hívja).

Általános irányelvek a build tervezéséhez

Mielőtt belemennénk a build plan elkészítésének részleteibe, fontos pár irányelvet megemlíteni, melyeket követve az elkészült build plan egyszerű és könnyen manage-elhető marad, vagy épp alapkövét képezi a CircleCI architektúrájának.

  1. A build egy containerben fut
    Bár már említettem is, de fontos hangsúlyozni, hogy minden "job" egy Docker containerben fog lefutni, így annak megfelelően kell összeállítanunk a lépéseket. Ugyan cache-elést bekapcsolhatunk a planeken, de arra kell számítanunk, hogy a build minden futtatásnál le fog tölteni minden függést. Továbbá nem elhanyagolható tény az sem, hogy a build-et futtató image képességeit tudjuk csak használni, így egy NodeJS-re tervezett build alapimage egyértelműen nem fog tudni Java alkalmazást fordítani és vica versa.

  2. Használjuk a rendszer által biztosított strukturálási lehetőségeket
    Buildjeink "job"-okra lesznek osztva, azok pedig "workflow"-kat képeznek, tehát egy workflow lényegében jobok sorba rendezett listája. A sorrendet a jobok között definiált függések határozzák meg. A workflow-kat beállíthatjuk különböző triggerekre, például különböző branchek és tagek különböző workflow végrehajtásokat eredményezhetnek, vagy akár időzített futtatást is beállíthatunk.

  3. DRY - Ne ismételjük magunkat a build planben sem
    Fejlesztők számára biztosan nem ismeretlen fogalom a DRY, azaz Don't Repeat Yourself elv, mely a kódismétlések elkerülését ösztönzi például kódrészletek kiemelésével, absztrakcióval. Mivel build planjeinkben is előfordulhatnak ismétlődő részletek, a CircleCI API-a lehetőséget ad azok kiszervezésére. Az egyik eszköz a már fentebb említett workflow, amivel egy-egy jobot több workflowban is felhasználhatunk. Lehetőségünk van továbbá parametrizálni azokat, ha mégis lennének apró eltérések (például a környezet neve). Komplett újrafelhasználható konfigurációs blokkok hozhatóak létre továbbá "command"-ok használatával, melyek szintén parametrizálhatóak. Ha már paraméterek, fontos megemlíteni, hogy a plan közösen használt paraméterei szintén kiszervezhetőek egy blokkba, érdemes azt is használni.

  4. Lehet, hogy már van megoldás arra, amit meg akarsz oldani
    Ez amolyan jó tanács, mielőtt belekezdünk egy saját plan összeállításába esetleg tömérdek mennyiségű Bash kód felhasználásával. A CircleCI ugyanis biztosít úgynevezett "orb"-okat, melyekkel gyakran használt build lépéseket tudunk pár sornyi konfigurációval elhelyezni saját planünkben. Az ilyeneket a CircleCI Orb Registry-jében böngészhetjük, mindegyikhez megfelelő leírás is található azok használatáról. Illetve, elkészíthetjük saját Orb leírónkat is, bár a registryben való elhelyezése teljesen publikussá teszi azt (legfeljebb a listázás tiltható).

Konfigurálás

A plan konfigurálása a projekt gyökerében, illetve az alatt egy .circleci nevű mappában elhelyezett config.yml file használatával történik. Nézzük meg sorban a konfiguráció fontosabb részeit (a teljes konfigurációt a cikk végén linkelem).

A konfiguráció a verzió definíciójával kezdődik (bár a sorrend mindegy, egy logikai sorrendet mindenképp érdemes fenntartani). A cikk írásakor a 2.1-es a frissebb API verzió, mely lehetővé teszi például az Orb-ok használatát, így érdemes inkább azt használni. Amennyiben biztosan nincs szükségünk ilyen funkciókra, használhatjuk a 2.0-s API verziót is, azonban így többek között a paraméterek és commandok használatától is elesünk.

version: 2.1

A továbbiakban (tetszőleges sorrendben) jöhetnek az orb, parameter, command, executor, job és workflow szekciók. Vegyük őket sorba:

Orb referenciák

Az orb-ok a CircleCI által biztosított újrafelhasználható build (rész)konfigurációk. Előnyük, hogy akár több száz sornyi konfigurációt is definiálhatnak, saját planünkben hivatkozva rájuk azokban csak néhány sornyi konfigurációt igényelnek. Az alábbiakban a JIRA integrációhoz készült Orb használata látható.

orbs:
  jira: circleci/jira@1.1.2

A fenti konfiguráció a CircleCI saját standard Orb registryjében található "JIRA" Orb 1.1.2-es verzióját hivatkozza. További teendőnk ezzel nincs, már csak a konfigurációját kell megadni később, ebben az esetben a workflow-ban (mivel az Orb által hozzáadott konfiguráció jobokat tartalmaz).

workflows:
  ...
  leaflet-backend-release:
    jobs:
      ...
      - build:  
          ...
          context: leaflet_ci
          post-steps:
            - jira/notify:
                environment_type: development
                job_type: build

A fenti konfiguráció a build nevű job lefutása után egy értesítést küld a kontextusban konfigurált JIRA számára. A kontextus konfigurálása fontos lépése minden buildnek, erre később még visszatérek!

Paraméterek

A paraméterek a build planek közösen használható, kiszervezett kulcs-érték párjai. Fontos megemlíteni, hogy a konfigurációban a paraméter típusa is megadható, a primitív típusok mellett (string, boolean, number) enum-ok is létrehozhatóak, mellyel lényegében megszorítás helyezhető 1-1 paraméter elfogadható értékeire.

parameters:
  app_name:
    type: string
    default: "leaflet"

Fent látható egy paraméter létrehozása, hivatkozni pedig bárhol lehet a build plan konfigurációjában az alábbi módon:

<< pipeline.parameters.app_name >>

Tehát mint az látható, a pipeline.parameters rész nem változik, a végén pedig a paraméter definiált neve áll. A plan feldolgozása előtt a feldolgozó végigfut a plan szövegén és a << ... >> közötti szövegrészeket kicseréli a definiált explicit értékekre. Fontos megemlíteni, hogy az általunk definiált paraméterek mellett a build plan kap néhány előredefiniált paramétert is, ezeket szintén használhatjuk a planben. Ilyenek például a végrehajtás sorszáma, Git információk, illetve a végrehajtás egyedi azonosítója - a használható paraméterek megtalálhatóak a hivatalos dokumentációban. Egy példa a << pipeline.number >>, mely a végrehajtás szekvenciális sorszáma. A különbség az általunk definiált paraméterekhez képest a parameters csoport hiánya.

Commandok

A commandok build stepek szekvenciái, melyek több jobban is újrahasznosíthatóak. Az alábbi command definíció egy paramétert vár, melynek értéke rc vagy release lehet, és a célja a build lefutása után egy új tag (release) létrehozása GitHub-on:

commands:
  github_release:
    parameters:
      release-type:
        type: enum
        default: "rc"
        enum: ["rc", "release"]
    steps:
      - attach_workspace:
          at: /tmp/ws_store
      - run:
          name: "Publish Release on GitHub"
          command: |
            [ [ "<< parameters.release-type >>" = "release" ]] && VERSION_QUALIFIER="-release" || VERSION_QUALIFIER=""
            VERSION=v$(cat /tmp/ws_store/version)$VERSION_QUALIFIER
            ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} \
                -c ${CIRCLE_SHA1} -delete ${VERSION} /tmp/ws_store/leaflet-backend-exec.jar

A command definícióban látható steps rész a jobok definíciójánál is fontos lesz, mivel az definiálja a jobban végrehajtandó build stepeket. Legtöbbször a step által végrehajtott command a run direktívával írható le, illetve látható egy speciális lépés is ez esetben, attach_workspace néven - később ezekre még visszatérünk.

A command ekkor még nem fut le, azt egy job stepként kell hivatkozni később, mely az alábbi módon történik:

jobs:
  ...
  publish-github-release:
    docker:
      - image: cibuilds/github:0.10
    steps:
      - github_release:
          release-type: release

A fenti részletben egy újabb érdekesség látható, méghozzá a docker szekció, mely egy executor.

Executorok

Az executorok a CircleCI build planek lelkei, lényegében egy executor definíció írja le, hogyan kell egy jobot lefuttatni. Ingyenes plant használva többnyire csak a Docker executort fogjuk használni, arra külön konfiguráció nem is szükséges, azonban létrehozhatunk akár úgy is saját executor definíciókat, hogy egy már létező executorra hivatkozunk és kibővítjük azt, például saját paraméterekkel.

Jobok

A jobok stepekből álló szekvenciák, melyek a build egy logikai egységének végrehajtásáért felelősek - például a forráskód beszerzése, fordítás, tesztvégrehajtás, telepítés, stb. Egy jobnak minden esetben definiálni kell, milyen executorral fut majd (ez a legtöbbször docker lesz, melyet egy standard a CircleCI által biztosított executor image definíciója követ), illetve definiálni kell a végrehajtandó lépéseket is. További paramétereket is megadhat a job, például environment variable-öket, meghatározhatja a munkakönyvtárat, szabályozhatja a párhuzamosítást a stepek között (amennyiben az lehetséges), stb.

Egy job definíciója látható alább:

jobs:
  build:
    docker:
      - image: cimg/openjdk:11.0.6
    steps:
      - checkout
      ...
      - run:
          command: mvn clean install -s .circleci/settings.xml
          name: Build
      - run:
          command: mvn -Duser.timezone=Europe/Budapest -pl acceptance-tests -Pacceptance verify -s .circleci/settings.xml
          name: Run Acceptance Tests
      - run:
          command: |
            mkdir -p /tmp/ws_store
            cp ./web/target/leaflet-backend-exec.jar /tmp/ws_store/leaflet-backend-exec.jar
            echo $PROJECT_VERSION >> /tmp/ws_store/version
          name: Prepare workspace shared storage
      - persist_to_workspace:
          root: /tmp/ws_store
          paths:
            - leaflet-backend-exec.jar
            - version
      - store_artifacts:
          path: web/target/leaflet-backend-exec.jar
      - store_test_results:
          path: web/target/surefire-reports

A fenti definícióban több érdekes és fontos dolog is történik, vegyük őket sorra:

  • A job definíciója a nevével kezdődik, mely ez esetben nemes egyszerűséggel "build".
  • A következő lépés az executor kiválasztása. Mivel ez a build egy Java alkalmazást fordít standard Linux-Docker környezetben, ezért a cimg/openjdk:11.0.6 executort választja ki. Az összes elérhető executor image megtekinthető a CircleCI DockerHub fiókján, azonban a dokumentáció alapján saját, privát baseimage-ek is készíthetőek.
  • Az első "speciális" lépés, amivel találkozni fogunk a checkout, mely a build planhez rendelt GitHub repository tartalmát klónozza a végrehajtó instance-ra. A fordítás ezután kezdődhet. Fontos megjegyezni, hogy erre a lépésre nem feltétlen lesz szükségünk, például deploy joboknál többnyire már csak a lefordított "termékekre" lesz szükségünk, például az executable JAR file-ra.
  • Ezután jönnek a custom commandok, a példában a fordítás és az acceptance tesztek futtatása látható, amelyeket az úgynevezett "workspace" előkészítése és populálása követ.
  • A workspace fontos eszközünk lesz, ugyanis azon keresztül tudunk perzisztens módon adatokat megosztani a jobok között. Két job között ugyanis nincs "adatkapcsolat", a jobok akár nem is egy végrehajtó gépen futnak, köztük adatok mozgatására az egyetlen lehetőségünk a workspace-ek használata.
  • Miután a workspace-t előkészítettük (elhelyeztük benne a perzisztálandó fájlokat), a persist_to_workspace lépéssel inicializáljuk azt. A root határozza meg a workspace által használt mappát, a paths a benne tárolandó fájlok nevét (relatív útvonalát). A workspace csatolása és a fájlok kiolvasása látható fentebb a github_release command definíciója alatt, mely az attach_workspace speciális step hívásával történik.
  • A store_artifacts és a store_test_results hosszabb távon tárolja a szerveren a létrejövő artifacteket, előbbi például az elkészült executable JAR-t, míg utóbbi a teszteredményeket. Ezek azonban nem azonosak a workspace funckiójával, tehát az így eltárolt artifactek nem hivatkozhatóak másik jobokban.

Workflow

A workflow az utolsó építőkő, mely már a jobokat foglalja össze. Alább látható egy példa a workflow beállítására.

workflows:
  ...
  leaflet-backend-release:
    jobs:

      - build:
          context: leaflet_ci
          filters:
            branches:
              only:
                - deploy
          post-steps:
            - jira/notify:
                environment_type: development
                job_type: build

      - deploy-approval:
          context: leaflet_ci
          type: approval
          requires:
            - build

      - deploy:
          context: leaflet_ci
          requires:
            - deploy-approval
          post-steps:
            - jira/notify:
                environment_type: production
                job_type: deployment

      - publish-github-release:
          context: leaflet_ci
          requires:
            - deploy

A következők történnek itt:

  • Definiálunk egy workflow-t, melynek neve leaflet-backend-release lesz.
  • A benne levő jobok a build, deploy-approval, deploy és publish-github-release.
  • A filter definícióval meghatározzuk, hogy a workflow csak a deploy nevű branchre való pushra triggerelődhet.
  • A requires direktívákkal függést állíthatunk fel a jobok között a workflowban, így látható, hogy a deploy-approval csak akkor futhat, ha a build sikeres lett, a deploy csak akkor ha a deploy-approval step lefutott, és így tovább.
  • Mivel a requires direktíva egyértelmű függést határoz meg a jobok között, elég, ha az első jobnál adják meg a filtert.
  • A deploy-approval egy speciális, a típusa szerint egy "approval" lépés, ami azt jelenti, hogy felhasználói beavatkozást fog várni a folytatás előtt. Az ilyen jobok nem igényelnek külön job definíciót, a build megállításán kívül másra nem használhatóak.

További hasznos információk

Build Context

Visszatérő paraméter a fenti konfigurációban a context, melynél ebben a planben minden esetben a leaflet_ci érték olvasható. A build context-ek a CircleCI fiókban létrehozható environment variable csomagok, melyekkel többek között jelszavak és egyéb, érzékeny konfigurációs paraméterek adhatóak át a build planek számára. Konfigurálásuk a CircleCI online felületén, az Organization Settings / Contexts menüpont alatt lehetséges. Érdekesség, hogy amennyiben a logokban megjelenik egy így átadott érték, azt minden esetben kicsillagozza a rendszer.

Build közben létrehozott környezeti változók

Build közben létrehozhatunk környezeti változókat, ám azok a stepek között nem továbbítódnak, így egy export command után hiába próbálnánk hivatkozni a létrehozott környezeti változóra egy másik stepben, üres értéket kapnánk. Megoldás azonban szerencsére van, mégpedig az alábbi trükk:

- run:
    command: echo 'export PROJECT_VERSION=`mvn help:evaluate -Dexpression=project.version -q -DforceStdout --non-recursive`' >> $BASH_ENV
    name: Extract project version

A fenti példában az alábbi lépések történnek:

  • A beágyazott mvn command kinyeri a project verzióját a POM-ból.
  • A verziószámot hozzárendeljük a PROJECT_VERSION változóhoz.
  • Exportáljuk az így létrejött változót.
  • És ezt az egész commandot redirecteljük a $BASH_ENV nevű változóba, melyet minden build step megkap a futtató környezettől (egy jobon belül).

A fenti módszerrel később a $PROJECT_VERSION változóra hivatkozva megkapjuk az extractolt Maven project verziót.

CircleCI CLI

A build plan létrehozása közben valószínűleg gyakran akarjuk majd azt tesztelni, ami azt jelentené ez esetben, hogy a plant minden tesztfuttatás előtt commitolnunk és pusholnunk kell a master branchre, ami egyrészt elég kényelmetlen és rengeteg szemetet hagy, másrészt meg pazaroljuk vele a kreditjeinket is. Szerencsére létezik egy CLI tool, mellyel lokális gépünkön is tesztelhetjük a változtatásokat. A tool Docker Desktop alatt használható, azonban sajnos jelenleg nem működik Windows környezeten, így a teszteléshez szükségünk lehet egy virtuális gépre, melyre Docker Engine-t telepítünk.

Egyedi build tool konfiguráció

Egyedi build tool konfiguráció használatára lehet számtalan esetben szükségünk. Szerencsére ezt megtehetjük, hiszen ami a repositoryban megtalálható, azt használhatjuk a build során. A Leaflet stack esetében egyedi Maven konfigurációra volt szükségem, mellyel egy külső Maven repository-t definiáltam a CircleCI számára. A konfigurációt egyszerűen csak adjuk hozzá a repositoryhoz, és ha bármilyen érzékeny adat található benne (felhasználónév, jelszó), azt a Build Context-ben is definiálhatjuk, a konfigurációs fájlban pedig csak a megfelelő hivatkozásokat kell elhelyeznünk.

Pipeline sorszám

A pipeline sorszáma (pipeline.number paraméter) megtévesztő lehet, jómagam ezzel szerettem volna helyettesíteni az eredeti Jenkins-es build sorszámot. Hamarosan kiderült, hogy nem pont ugyanúgy viselkedik ez a sorszám, mint Jenkins alatt. Alapesetben a sorszám akkor növekszik egyel, ha triggerelődik egy build - vagyis ez pont jó lenne, hiszen a filter szerint csak a deploy branchre történő pushok triggerelnek buildet. A helyzet azonban nem pont ez, ugyanis minden push triggerelni fog egy buildet, de csak a filter kifejezések által végrehajtható pipeline-ok fognak ténylegesen le is futni. Az összes többi csupán egy értesítést hagy a build historyban, miszerint a build nem futott le a meghatározott filterek alapján. Így persze nem egy pontosan növekvő szekvenciát kapunk, az aktivitástól függően lesz hogy csak egyel növekszik az érték, de lesz olyan is, hogy sokkal többel. Megoldásképp a repositoryban létrehozott tageket használom, az azokban található build számot extractolja egy erre létrehozott build step, majd növeli egyel és tageli a következő buildet az így megnövelt számmal.

Konklúzió

Bár nem hibátlan, de a CircleCI egy remek alternatíva a saját magunk által fenntartunk CI/CD megoldások helyett. Természetesen nem ez az egyetlen lehetőségünk, GitHub-on használhatjuk például a GitHub Actionst hasonló célokra, illetve 3rd party megoldások között ott van például a TravisCI is, de továbbiakat is találhatunk még a marketplace-en.

Teljes CircleCI build plan a Leaflet Backend alkalmazáshoz

CircleCI a GitHub MarketPlace-en

CircleCI dokumentáció

Kommentek
Hozzászólok

Még senki nem szólt hozzá ehhez a bejegyzéshez.